【仓颉纪元】仓颉鸿蒙应用深度开发:待办事项 App 全流程实战
学习仓颉语言的最终目的是开发鸿蒙原生应用。作为一名移动应用开发工程师,我通过开发待办事项 App 完整实践了鸿蒙应用开发的全流程。从环境搭建到组件开发,从状态管理到数据持久化,从页面导航到分布式能力,每个环节都经过深入探索和实战验证。ArkUI 的声明式语法让 UI 开发简洁直观,响应式状态管理让数据流动清晰可控,分布式能力让跨设备协同变得简单。本文将以待办事项 App 为例,系统讲解鸿蒙应用开发
文章目录
前言
学习仓颉语言的最终目的是开发鸿蒙原生应用。作为一名移动应用开发工程师,我通过开发待办事项 App 完整实践了鸿蒙应用开发的全流程。从环境搭建到组件开发,从状态管理到数据持久化,从页面导航到分布式能力,每个环节都经过深入探索和实战验证。ArkUI 的声明式语法让 UI 开发简洁直观,响应式状态管理让数据流动清晰可控,分布式能力让跨设备协同变得简单。本文将以待办事项 App 为例,系统讲解鸿蒙应用开发的核心技术点,包括 ArkUI 组件体系、状态管理方案、数据持久化方法、分布式数据同步等内容,并分享开发过程中的实战经验和踩坑心得。通过本文,你将掌握使用仓颉开发功能完整的鸿蒙原生应用的能力。
声明:本文由作者“白鹿第一帅”于 CSDN 社区原创首发,未经作者本人授权,禁止转载!爬虫、复制至第三方平台属于严重违法行为,侵权必究。亲爱的读者,如果你在第三方平台看到本声明,说明本文内容已被窃取,内容可能残缺不全,强烈建议您移步“白鹿第一帅” CSDN 博客查看原文,并在 CSDN 平台私信联系作者对该第三方违规平台举报反馈,感谢您对于原创和知识产权保护做出的贡献!
文章作者:白鹿第一帅,作者主页:https://blog.csdn.net/qq_22695001,未经授权,严禁转载,侵权必究!
一、开发环境搭建
1.1、DevEco Studio 环境配置
安装步骤详解
| 步骤 | 操作 | 时间 | 注意事项 |
|---|---|---|---|
| 1 | 下载 DevEco Studio | 10-20 分钟 | 约 2GB,需要稳定网络 |
| 2 | 安装 IDE | 5-10 分钟 | 选择合适的安装路径 |
| 3 | 安装仓颉插件 | 3-5 分钟 | 在插件市场搜索“Cangjie” |
| 4 | 配置 SDK | 15-30 分钟 | 下载 API 10 及以上版本 |
| 5 | 创建项目 | 2-3 分钟 | 选择“Empty Ability”模板 |
- 下载 DevEco Studio(https://developer.huawei.com/consumer/cn/deveco-studio/)
- 安装仓颉语言插件
- 配置 HarmonyOS SDK
- 创建仓颉项目
1.2、鸿蒙项目结构解析
目录结构说明
| 目录/文件 | 用途 | 重要性 |
|---|---|---|
pages/ |
应用页面 | ⭐⭐⭐⭐⭐ |
components/ |
可复用组件 | ⭐⭐⭐⭐ |
models/ |
数据模型 | ⭐⭐⭐⭐ |
services/ |
业务服务 | ⭐⭐⭐ |
utils/ |
工具类 | ⭐⭐⭐ |
resources/ |
资源文件 | ⭐⭐⭐⭐ |
module.json5 |
模块配置 | ⭐⭐⭐⭐⭐ |
cangjie.toml |
仓颉配置 | ⭐⭐⭐⭐⭐ |
my-harmony-app/
├── src/
│ ├── main/
│ │ ├── cj/ # 仓颉源代码
│ │ │ ├── pages/ # 页面
│ │ │ ├── components/ # 组件
│ │ │ ├── models/ # 数据模型
│ │ │ ├── services/ # 业务服务
│ │ │ └── utils/ # 工具类
│ │ ├── resources/ # 资源文件
│ │ │ ├── base/
│ │ │ │ ├── element/
│ │ │ │ ├── media/
│ │ │ │ └── profile/
│ │ └── module.json5 # 模块配置
├── cangjie.toml # 仓颉配置
└── build-profile.json5 # 构建配置
二、ArkUI 组件开发
ArkUI 是鸿蒙的声明式 UI 框架,采用类似 Flutter 和 SwiftUI 的设计理念。
声明式 vs 命令式 UI
| 对比项 | 命令式 UI | 声明式 UI | 优势 |
|---|---|---|---|
| 代码量 | 多 | 少 | 声明式减少 50%+ |
| 可读性 | 低 | 高 | 更易理解 |
| 维护性 | 难 | 易 | 更易维护 |
| 性能 | 手动优化 | 自动优化 | 框架智能优化 |
| 学习曲线 | 陡峭 | 平缓 | 更易上手 |
声明式 UI 的核心思想是描述“UI 是什么样子”而不是“如何更新 UI”,框架会自动处理 UI 的更新和渲染优化。这种编程范式让代码更简洁、更易维护,也更符合现代 UI 开发的趋势。
2.1、ArkUI 基础组件应用
基础组件是构建 UI 的基石。在待办事项 App 中,最常用的是 Text(显示文本)、Button(触发操作)、TextInput(输入内容)、Image(显示图标)这几个组件。
基础组件对比
| 组件 | 用途 | 常用属性 | 使用频率 |
|---|---|---|---|
| Text | 显示文本 | fontSize, fontColor, fontWeight | ⭐⭐⭐⭐⭐ |
| Button | 触发操作 | onClick, backgroundColor | ⭐⭐⭐⭐⭐ |
| TextInput | 输入内容 | placeholder, onChange | ⭐⭐⭐⭐ |
| Image | 显示图片 | src, width, height | ⭐⭐⭐ |
ArkUI 采用链式调用方式设置属性,代码简洁易读。
下面的示例展示了 Text 组件的多种样式设置,包括字体大小、颜色、粗细等常用属性。这些属性可以灵活组合,满足不同的 UI 需求:
// Text组件:显示不同样式的文本
@Component // @Component装饰器标记这是一个可复用的UI组件
struct TextDemo {
// build()方法是组件的核心,定义了UI的结构和样式
func build() {
// Column是垂直布局容器,子组件从上到下排列
Column() {
// Text组件用于显示文本,通过链式调用设置样式属性
Text("普通文本")
.fontSize(16) // 设置字体大小为16sp
.fontColor(Color.Black) // 设置字体颜色为黑色
Text("粗体文本")
.fontSize(18) // 字体大小18sp
.fontWeight(FontWeight.Bold) // 设置字体粗细为粗体
Text("彩色文本")
.fontSize(20) // 字体大小20sp
.fontColor(Color.Blue) // 设置字体颜色为蓝色
}
}
}
Button 组件用于触发用户操作,支持点击事件回调。下面的计数器示例展示了如何使用 @State 管理按钮的状态,每次点击都会触发状态更新,UI 会自动重新渲染:
// Button组件:实现计数器功能
@Component
struct ButtonDemo {
// @State装饰器标记响应式状态变量,当count变化时UI会自动更新
@State private var count: Int32 = 0
func build() {
// Column的spacing参数设置子组件之间的垂直间距为10vp
Column(spacing: 10) {
// 使用字符串插值${count}动态显示计数值
Text("点击次数: ${count}").fontSize(20)
// Button组件的onClick接收一个回调函数,点击时执行
// 使用lambda表达式() => { ... }定义回调逻辑
Button("点击我")
.onClick(() => {
count += 1 // 每次点击计数加1,触发UI自动刷新
})
// 可以为Button设置背景色等样式属性
Button("重置")
.backgroundColor(Color.Red) // 设置按钮背景为红色
.onClick(() => {
count = 0 // 重置计数为0
})
}
}
}
TextInput 组件接收用户输入,通过 onChange 回调实时监听输入变化。在待办事项 App 中,用户通过它输入新任务的标题和描述:
// TextInput组件:实时监听用户输入
@Component
struct InputDemo {
// 使用@State管理输入框的文本内容
@State private var text: String = ""
func build() {
Column(spacing: 10) {
// TextInput是输入框组件,placeholder显示占位提示文本
TextInput(placeholder: "请输入内容")
// onChange回调在用户每次输入时触发
// value参数包含输入框的最新内容
.onChange((value: String) => {
text = value // 将输入内容同步到状态变量
})
// 实时显示用户输入的内容,展示响应式更新效果
Text("输入的内容: ${text}").fontSize(16)
}
}
}
2.2、响应式布局设计
布局是 UI 开发的核心,决定了界面的结构和视觉效果。
布局组件选择指南
| 布局组件 | 使用场景 | 性能 | 复杂度 |
|---|---|---|---|
| Column | 垂直排列 | ⭐⭐⭐⭐⭐ | ⭐ |
| Row | 水平排列 | ⭐⭐⭐⭐⭐ | ⭐ |
| Stack | 组件叠加 | ⭐⭐⭐⭐ | ⭐⭐ |
| Grid | 网格布局 | ⭐⭐⭐⭐ | ⭐⭐⭐ |
| List | 长列表 | ⭐⭐⭐⭐⭐ | ⭐⭐ |
ArkUI 提供了多种布局组件:Column(垂直布局)用于从上到下排列子组件,Row(水平布局)用于从左到右排列,Stack(层叠布局)用于组件叠加显示,Grid(网格布局)用于规则的网格排列,List(列表布局)专门用于长列表的高性能渲染。
待办事项 App 布局结构
在待办事项 App 中,主界面使用 Column 垂直排列输入框和任务列表,每个任务项使用 Row 横向排列复选框、文本和删除按钮,任务列表使用 List 组件实现虚拟滚动。下面的示例展示了 Column 和 Row 的基本用法,通过 spacing 设置子组件间距,通过 justifyContent 和 alignItems 控制对齐方式:
// Column垂直布局:子组件从上到下排列,支持居中对齐
@Component
struct ColumnDemo {
func build() {
// spacing: 20 设置子组件之间的垂直间距为20vp
Column(spacing: 20) {
Text("第一行")
Text("第二行")
Text("第三行")
}
.width("100%") // 设置宽度为父容器的100%
// justifyContent控制主轴(垂直方向)的对齐方式
// FlexAlign.Center表示子组件在垂直方向居中
.justifyContent(FlexAlign.Center)
// alignItems控制交叉轴(水平方向)的对齐方式
// HorizontalAlign.Center表示子组件在水平方向居中
.alignItems(HorizontalAlign.Center)
}
}
Row 布局用于横向排列组件,常用于工具栏、按钮组等场景。通过 justifyContent 可以控制子组件的分布方式,SpaceBetween 表示两端对齐,中间平均分布:
// Row水平布局:子组件从左到右排列,支持空间分配
@Component
struct RowDemo {
func build() {
// spacing: 10 设置子组件之间的水平间距为10vp
Row(spacing: 10) {
Text("左")
Text("中")
Text("右")
}
.width("100%") // 占满父容器宽度
// FlexAlign.SpaceBetween表示两端对齐
// 第一个子组件靠左,最后一个靠右,中间的平均分布剩余空间
.justifyContent(FlexAlign.SpaceBetween)
}
}
List 组件是长列表的最佳选择,支持虚拟滚动和懒加载,即使有上千条数据也能保持流畅。在待办事项 App 中,任务列表就是用 List 实现的,每个 ListItem 代表一个任务项:
// List列表布局:专门用于长列表,支持虚拟滚动优化性能
@Component
struct ListDemo {
// 定义列表数据源
private var items: Array<String> = ["项目1", "项目2", "项目3", "项目4", "项目5"]
func build() {
// List组件自动实现虚拟滚动,只渲染可见区域的列表项
// 即使有成千上万条数据,也能保持流畅的滚动性能
List() {
// 使用for循环遍历数据源,为每个数据项创建ListItem
for (item in items) {
// ListItem是List的子组件,代表列表中的一项
ListItem() {
// 每个列表项显示一个Text组件
Text(item)
.width("100%") // 占满列表项宽度
.height(50) // 设置固定高度50vp
.padding(10) // 内边距10vp,让文本不贴边
}
}
}
.width("100%") // List占满父容器宽度
.height("100%") // List占满父容器高度,超出部分可滚动
}
}
2.3、自定义组件封装
当 UI 元素需要重复使用时,自定义组件是最佳选择。在待办事项 App 中,任务项卡片在列表中多次出现,将其封装为自定义组件可以实现代码复用、简化维护、提高可读性。自定义组件通过构造函数接收参数,通过回调函数响应用户操作。
下面的 UserCard 组件展示了自定义组件的完整实现。组件接收用户信息作为参数,通过 Row 和 Column 组合实现卡片布局,通过 onClick 回调处理点击事件。使用时只需传入数据和回调函数即可:
// 自定义用户卡片组件:封装可复用的UI元素
@Component // 标记为可复用组件
public struct UserCard {
// 组件的属性,通过构造函数传入
private var name: String // 用户名
private var avatar: String // 头像URL
private var bio: String // 个人简介
private var onTap: () -> Unit // 点击回调函数
// 构造函数:创建组件实例时初始化属性
public init(name: String, avatar: String, bio: String, onTap: () -> Unit) {
this.name = name
this.avatar = avatar
this.bio = bio
this.onTap = onTap
}
func build() {
// 使用Row实现横向布局:头像在左,信息在右
Row(spacing: 15) { // 子组件间距15vp
// Image组件显示圆形头像
Image(avatar)
.width(60)
.height(60)
.borderRadius(30) // 圆角半径为宽高的一半,形成圆形
// Column垂直排列用户名和简介
Column(spacing: 5) {
// 用户名:粗体、较大字号
Text(name)
.fontSize(18)
.fontWeight(FontWeight.Bold)
// 个人简介:灰色、较小字号、最多显示2行
Text(bio)
.fontSize(14)
.fontColor(Color.Gray)
.maxLines(2) // 超过2行会被截断
}
.alignItems(HorizontalAlign.Start) // 文本左对齐
.layoutWeight(1) // 占据Row中剩余的所有空间
}
.width("100%") // 卡片占满父容器宽度
.padding(15) // 内边距15vp
.backgroundColor(Color.White) // 白色背景
.borderRadius(10) // 圆角10vp
.onClick(onTap) // 点击时触发回调函数
}
}
使用自定义组件时,只需在 List 中遍历数据,为每个用户创建一个 UserCard 实例。这种方式让代码结构清晰,易于维护和扩展:
// 使用自定义组件构建用户列表
@Component
struct UserListPage {
// 加载用户数据(假设loadUsers()从数据库或网络获取数据)
private var users: Array<User> = loadUsers()
func build() {
List() {
// 遍历用户数组,为每个用户创建一个卡片
for (user in users) {
ListItem() {
// 使用自定义的UserCard组件
// 通过命名参数传递数据,代码清晰易读
UserCard(
name: user.name, // 传递用户名
avatar: user.avatar, // 传递头像URL
bio: user.bio, // 传递个人简介
// onTap回调:点击卡片时跳转到用户详情页
onTap: () => {
navigateToUserDetail(user.id)
}
)
}
}
}
.padding(10) // 列表整体内边距10vp
}
}
三、状态管理
状态是界面上会变化的数据,比如任务列表、任务完成状态、输入框内容等。
响应式编程模型
状态管理层次
| 装饰器 | 用途 | 数据流 | 使用场景 |
|---|---|---|---|
@State |
组件内部状态 | 单向 | 计数器、开关 |
@Prop |
父传子(只读) | 单向 | 配置、属性 |
@Link |
父子双向绑定 | 双向 | 表单、输入 |
@Observed |
对象可观察 | - | 复杂数据 |
@ObjectLink |
对象引用 | 双向 | 对象状态 |
AppStorage |
全局状态 | - | 用户信息 |
ArkUI 采用响应式编程模型,当状态改变时 UI 会自动更新,无需手动操作 DOM。状态管理分为三个层次:@State 管理组件内部状态,@Prop/@Link 用于父子组件间传递状态,@Observed/@ObjectLink 用于复杂对象的状态管理。
3.1、@State 本地状态管理
@State 用于管理组件内部状态,当状态变化时 ArkUI 会自动重新渲染相关 UI。在待办事项 App 中,用它管理新任务的输入内容、列表的展开状态、编辑模式开关等。下面的计数器示例展示了 @State 的基本用法,每次点击按钮都会触发状态更新,UI 自动响应变化:
// @State管理组件内部状态,实现响应式UI更新
@Component
struct CounterPage {
// @State装饰器让变量成为响应式状态
// 当状态变化时,ArkUI会自动重新渲染使用该状态的UI部分
@State private var count: Int32 = 0 // 计数器的值
@State private var isRunning: Bool = false // 运行状态开关
func build() {
Column(spacing: 20) {
// 显示当前计数值,使用字符串插值${count}
Text("计数: ${count}")
.fontSize(32)
.fontWeight(FontWeight.Bold)
// 三个按钮横向排列
Row(spacing: 10) {
// 点击"增加"按钮,count自增1,UI自动更新
Button("增加").onClick(() => { count += 1 })
// 点击"减少"按钮,count自减1
Button("减少").onClick(() => { count -= 1 })
// 点击"重置"按钮,count归零
Button("重置").onClick(() => { count = 0 })
}
// Toggle开关组件,使用$符号实现双向绑定
// $isRunning表示将isRunning的引用传递给Toggle
// Toggle的状态变化会自动同步到isRunning变量
Toggle(isOn: $isRunning)
// 根据isRunning的值显示不同文本
// 使用三元运算符实现条件渲染
Text(isRunning ? "运行中" : "已停止")
}
.width("100%")
.height("100%")
.justifyContent(FlexAlign.Center) // 内容垂直居中
}
}
3.2、@Prop/@Link 父子组件通信
父子组件间需要传递数据时,使用 @Prop 和 @Link。@Prop 实现单向传递,子组件只能读取不能修改父组件数据,适合纯展示场景。@Link 实现双向绑定,子组件可以修改父组件数据,适合输入框、开关等交互组件。在待办事项 App 中,任务列表项用 @Prop 显示任务信息,任务编辑页面用 @Link 修改任务数据。
下面的示例展示了两种传递方式的区别。ChildComponent1 使用 @Prop 接收数据,只能读取不能修改。ChildComponent2 使用 @Link 绑定数据,可以直接修改父组件的 message 变量,父组件会自动响应变化:
// 父子组件通信:@Prop单向传递,@Link双向绑定
@Component
struct ParentComponent {
// 父组件持有状态数据
@State private var message: String = "Hello"
func build() {
Column() {
// 方式1:使用@Prop单向传递
// 直接传递message的值,子组件只能读取不能修改
ChildComponent1(text: message)
// 方式2:使用@Link双向绑定
// 使用$message传递引用,子组件可以修改父组件的数据
ChildComponent2(text: $message)
// 父组件修改message,两个子组件都会自动更新
Button("修改消息").onClick(() => { message = "World" })
}
}
}
// 子组件1:使用@Prop接收数据(只读)
@Component
struct ChildComponent1 {
// @Prop表示这是从父组件传入的属性
// 子组件只能读取text的值,不能修改
@Prop private var text: String
func build() {
Text("接收到: ${text}") // 显示父组件传入的值
}
}
// 子组件2:使用@Link绑定数据(可读写)
@Component
struct ChildComponent2 {
// @Link表示这是与父组件双向绑定的引用
// 子组件修改text时,父组件的message也会同步更新
@Link private var text: String
func build() {
Column() {
Text("当前值: ${text}") // 显示当前值
// 子组件可以直接修改text
// 修改后父组件的message和ChildComponent1都会自动更新
Button("修改").onClick(() => { text = "Modified" })
}
}
}
3.3、@Observed/@ObjectLink 复杂对象状态
当需要管理复杂对象时,使用 @Observed 和 @ObjectLink。@Observed 标记类为可观察对象,当对象属性变化时会通知 UI 更新。@ObjectLink 用于子组件中引用可观察对象,实现对象级别的响应式更新。在待办事项 App 中,TodoItem 和 TodoStore 都是可观察对象,任何属性变化都会自动触发 UI 刷新。
下面的代码展示了完整的待办事项管理。TodoStore 管理任务列表,提供添加、切换完成状态、删除等方法。TodoListPage 使用 @State 持有 store 实例,TodoItemView 使用 @ObjectLink 引用单个任务项,实现细粒度的 UI 更新:
// 可观察的数据模型:任务项和任务存储
// @Observed装饰器让类成为可观察对象
// 当对象的属性发生变化时,会自动通知UI进行更新
@Observed
class TodoItem {
var id: Int64 // 任务的唯一标识符
var title: String // 任务标题
var completed: Bool // 任务完成状态
// 构造函数:创建新任务时初始化属性
// completed参数有默认值false,表示新任务默认未完成
init(id: Int64, title: String, completed: Bool = false) {
this.id = id
this.title = title
this.completed = completed
}
}
// TodoStore是任务管理的核心类,负责所有任务的增删改查
@Observed
class TodoStore {
var items: Array<TodoItem> = [] // 存储所有任务的数组
var nextId: Int64 = 1 // 自增ID,确保每个任务有唯一标识
// 添加新任务:创建TodoItem实例并添加到数组
func addItem(title: String): Unit {
// 使用当前nextId创建新任务
items.append(TodoItem(nextId, title))
// ID自增,为下一个任务准备
nextId += 1
}
// 切换任务完成状态:根据ID查找任务并反转completed属性
func toggleItem(id: Int64): Unit {
// 遍历任务数组查找目标任务
for (item in items) {
if (item.id == id) {
// 使用!运算符反转布尔值:true变false,false变true
item.completed = !item.completed
break // 找到后立即退出循环,提高性能
}
}
}
// 删除任务:使用filter方法过滤掉指定ID的任务
func removeItem(id: Int64): Unit {
// filter返回一个新数组,只包含id不等于指定值的任务
// lambda表达式{ item => item.id != id }定义过滤条件
items = items.filter({ item => item.id != id })
}
}
TodoListPage 是主界面,包含输入框和任务列表。用户在输入框中输入任务标题,点击添加按钮后调用 store.addItem() 添加任务。任务列表使用 List 组件渲染,每个任务项由 TodoItemView 组件展示:
// 待办事项列表页面:管理任务的添加和展示
@Component
struct TodoListPage {
// @State管理TodoStore实例,当store中的数据变化时UI会自动更新
@State private var store: TodoStore = TodoStore()
// @State管理输入框的文本内容
@State private var newTodoText: String = ""
func build() {
Column() {
// 顶部输入区域:输入框和添加按钮横向排列
Row(spacing: 10) {
// TextInput占据Row中的剩余空间
TextInput(placeholder: "添加待办事项")
.layoutWeight(1) // 权重为1,自动填充剩余空间
// onChange实时监听输入变化,将内容同步到newTodoText
.onChange((value: String) => { newTodoText = value })
// 添加按钮:点击时创建新任务
Button("添加").onClick(() => {
// 验证输入不为空
if (newTodoText != "") {
// 调用store的addItem方法添加任务
store.addItem(newTodoText)
// 清空输入框,准备下一次输入
newTodoText = ""
}
})
}
.padding(10) // 输入区域内边距
// 任务列表:使用List组件展示所有任务
List() {
// 遍历store中的所有任务
for (item in store.items) {
ListItem() {
// 为每个任务创建TodoItemView组件
// 传入任务对象和store引用,以便操作任务
TodoItemView(item: item, store: store)
}
}
}
}
}
}
TodoItemView 展示单个任务项,包含复选框、标题和删除按钮。使用 @ObjectLink 引用任务项,当任务状态变化时只更新对应的UI部分,不会重新渲染整个列表,性能更优:
// 任务项视图:展示单个任务的详细信息
@Component
struct TodoItemView {
// @ObjectLink用于引用可观察对象
// 当item的属性变化时,只有这个TodoItemView会重新渲染
// 不会影响列表中的其他任务项,实现细粒度的UI更新
@ObjectLink private var item: TodoItem
// 持有store引用,用于调用toggleItem和removeItem方法
private var store: TodoStore
// 构造函数:接收任务对象和store引用
init(item: TodoItem, store: TodoStore) {
this.item = item
this.store = store
}
func build() {
// Row横向布局:复选框、标题、删除按钮从左到右排列
Row(spacing: 10) {
// Checkbox复选框,显示任务的完成状态
Checkbox(selected: item.completed)
// onChange回调:用户点击复选框时触发
.onChange((checked: Bool) => {
// 调用store的toggleItem方法切换任务状态
store.toggleItem(item.id)
})
// 显示任务标题
Text(item.title)
.fontSize(16)
// decoration根据完成状态动态设置文本装饰
// 已完成的任务显示删除线,未完成的任务无装饰
.decoration(
item.completed
? TextDecorationType.LineThrough // 删除线
: TextDecorationType.None // 无装饰
)
.layoutWeight(1) // 占据Row中的剩余空间
// 删除按钮:点击时从列表中移除任务
Button("删除")
.backgroundColor(Color.Red) // 红色背景表示危险操作
.onClick(() => {
// 调用store的removeItem方法删除任务
store.removeItem(item.id)
})
}
.width("100%") // 任务项占满列表宽度
.padding(10) // 内边距10vp
}
}
四、页面导航
页面导航实现应用内的页面跳转和参数传递。router.pushUrl() 跳转到新页面并保留当前页面,router.replaceUrl() 跳转并替换当前页面,router.back() 返回上一页。跳转时可以通过 params 传递参数,目标页面通过 router.getParams() 获取参数。
4.1、Router 页面跳转与参数传递
下面的示例展示了页面跳转的完整流程。HomePage 通过 pushUrl 跳转到 DetailPage 并传递 id 和 name 参数,DetailPage 在 aboutToAppear 生命周期中获取参数并显示,用户点击返回按钮调用 router.back() 返回上一页:
// 主页:实现页面跳转和参数传递
@Entry // @Entry标记这是应用的入口页面
@Component
struct HomePage {
func build() {
Column(spacing: 20) {
Text("主页").fontSize(24)
// 点击按钮跳转到详情页
Button("跳转到详情页").onClick(() => {
// router.pushUrl()实现页面跳转
// pushUrl会保留当前页面在导航栈中,用户可以返回
router.pushUrl({
url: "pages/DetailPage", // 目标页面路径
// params对象传递参数到目标页面
params: {
id: 123, // 传递ID参数
name: "示例" // 传递名称参数
}
})
})
}
}
}
// 详情页:接收路由参数并展示
@Entry // @Entry标记这也是一个可独立访问的页面
@Component
struct DetailPage {
// 使用@State管理从路由接收的参数
@State private var id: Int64 = 0
@State private var name: String = ""
// aboutToAppear是页面生命周期方法
// 在页面即将显示时调用,适合在这里获取路由参数
func aboutToAppear() {
// router.getParams()获取跳转时传递的参数对象
let params = router.getParams()
// 从params中提取参数,使用as进行类型转换
id = params["id"] as Int64
name = params["name"] as String
}
func build() {
Column(spacing: 20) {
Text("详情页").fontSize(24)
// 显示接收到的参数
Text("ID: ${id}")
Text("名称: ${name}")
// 返回按钮:调用router.back()返回上一页
Button("返回").onClick(() => {
router.back() // 从导航栈中弹出当前页面,返回HomePage
})
}
}
}
4.2、Tab 底部导航栏实现
Tab 导航用于实现底部导航栏,常见于首页、发现、消息、我的等多标签页应用。Tabs 组件管理多个 TabContent,每个 TabContent 对应一个页面,通过 @State 绑定 currentIndex 实现标签切换:
// Tab导航:实现底部导航栏的多标签页切换
@Entry
@Component
struct MainPage {
// @State管理当前选中的标签页索引
// 0表示第一个标签页,1表示第二个,以此类推
@State private var currentIndex: Int32 = 0
func build() {
// Tabs组件管理多个标签页
// 使用$currentIndex双向绑定当前索引
Tabs(index: $currentIndex) {
// 每个TabContent代表一个标签页
// tabBar()方法设置标签页的标题
TabContent() {
HomePage() // 首页内容
}.tabBar("首页")
TabContent() {
DiscoverPage() // 发现页内容
}.tabBar("发现")
TabContent() {
MessagePage() // 消息页内容
}.tabBar("消息")
TabContent() {
ProfilePage() // 我的页面内容
}.tabBar("我的")
}
// barPosition设置标签栏位置
// BarPosition.End表示在底部(移动端常见布局)
// BarPosition.Start表示在顶部
.barPosition(BarPosition.End)
// onChange监听标签页切换事件
// 当用户点击不同标签时,index参数包含新的索引值
.onChange((index: Int32) => {
currentIndex = index // 更新当前索引
})
}
}
五、数据持久化
数据持久化让应用数据在关闭后仍然保留。鸿蒙提供三种方案:Preferences 适合轻量级键值对存储(如配置、设置),关系型数据库适合结构化数据和复杂查询(如任务、笔记),分布式数据库支持跨设备同步。在待办事项 App 中,用 Preferences 存储用户设置,用关系型数据库存储任务列表。
5.1、Preferences 轻量级键值存储
Preferences 是键值对存储,使用简单、读写快速、自动持久化,适合存储用户名、主题设置、排序方式等配置信息。下面的 PreferencesManager 封装了常用操作,提供 putString/putInt/putBool 保存数据,getString/getInt/getBool 读取数据,每次写入后调用 flush() 确保数据持久化:
// Preferences管理器:封装轻量级键值对存储
// 适合存储用户设置、配置信息等简单数据
class PreferencesManager {
private var preferences: Preferences
// 构造函数:初始化Preferences实例
init() {
// getInstance()获取或创建名为"app_settings"的Preferences实例
// 同一个名称在整个应用中共享同一个实例
this.preferences = Preferences.getInstance("app_settings")
}
// 保存字符串类型的键值对
func putString(key: String, value: String): Unit {
// putString()将数据写入内存
preferences.putString(key, value)
// flush()将内存中的数据持久化到磁盘
// 必须调用flush()才能确保数据在应用关闭后仍然保留
preferences.flush()
}
// 读取字符串类型的值
// defaultValue参数提供默认值,当key不存在时返回
func getString(key: String, defaultValue: String = ""): String {
return preferences.getString(key, defaultValue)
}
// 保存布尔类型的键值对
func putBool(key: String, value: Bool): Unit {
preferences.putBool(key, value)
preferences.flush() // 持久化到磁盘
}
// 读取布尔类型的值
func getBool(key: String, defaultValue: Bool = false): Bool {
return preferences.getBool(key, defaultValue)
}
}
SettingsPage 展示了 Preferences 的实际应用。在 aboutToAppear 生命周期中加载保存的设置,在用户修改时实时保存。这种方式让用户设置在应用重启后依然有效:
// 设置页面:使用Preferences保存和加载用户配置
@Component
struct SettingsPage {
// 创建PreferencesManager实例用于数据持久化
private var prefs: PreferencesManager = PreferencesManager()
// @State管理用户名和深色模式开关状态
@State private var username: String = ""
@State private var darkMode: Bool = false
// aboutToAppear生命周期方法:页面即将显示时调用
// 在这里加载之前保存的设置
func aboutToAppear() {
// 从Preferences中读取用户名,如果不存在则返回空字符串
username = prefs.getString("username")
// 从Preferences中读取深色模式设置,如果不存在则返回false
darkMode = prefs.getBool("dark_mode")
}
func build() {
Column(spacing: 20) {
// 用户名输入框
TextInput(placeholder: "用户名", text: username)
// onChange实时监听输入变化
.onChange((value: String) => {
// 更新状态变量
username = value
// 立即保存到Preferences,实现自动保存
prefs.putString("username", value)
})
// 深色模式开关
Row() {
Text("深色模式")
// Toggle开关组件,使用$darkMode双向绑定
Toggle(isOn: $darkMode)
// onChange监听开关状态变化
.onChange((value: Bool) => {
// 保存深色模式设置到Preferences
prefs.putBool("dark_mode", value)
})
}
}
}
}
5.2、关系型数据库操作
关系型数据库适合存储结构化数据,支持 SQL 查询、事务处理、索引优化。在待办事项 App 中,用它存储任务列表,支持按时间、优先级等条件查询和排序。DatabaseHelper 封装了数据库操作,在初始化时创建表结构,提供增删改查方法。
下面的代码展示了完整的数据库操作流程。createTables() 创建 users 表,insertUser() 插入新用户,queryUsers() 查询所有用户并转换为对象数组,updateUser() 和 deleteUser() 实现更新和删除功能:
// 数据库助手:封装关系型数据库的增删改查操作
// 适合存储结构化数据,支持复杂查询和事务处理
class DatabaseHelper {
private var db: RdbStore // 关系型数据库实例
// 构造函数:初始化数据库连接并创建表结构
init() {
// 配置数据库:指定数据库文件名和安全级别
// SecurityLevel.S1表示最低安全级别,适合一般应用数据
let config = RdbStoreConfig(name: "app.db", securityLevel: SecurityLevel.S1)
// 获取或创建数据库实例
this.db = RdbStore.getRdbStore(config)
// 创建表结构
createTables()
}
// 创建数据库表
private func createTables(): Unit {
// 使用三引号"""定义多行SQL语句
let sql = """
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT, -- 自增主键
name TEXT NOT NULL, -- 用户名,不能为空
email TEXT UNIQUE, -- 邮箱,唯一约束
age INTEGER, -- 年龄
created_at INTEGER -- 创建时间戳
)
"""
// executeSql()执行SQL语句
db.executeSql(sql)
}
// 插入新用户:返回新插入记录的ID
func insertUser(name: String, email: String, age: Int32): Int64 {
// ValuesBucket是键值对容器,用于构建插入数据
let values = ValuesBucket()
values.putString("name", name) // 设置name字段
values.putString("email", email) // 设置email字段
values.putInt("age", age) // 设置age字段
// 自动设置创建时间为当前时间戳
values.putLong("created_at", Time.currentTimeMillis())
// insert()方法插入数据,返回新记录的ID
return db.insert("users", values)
}
// 查询所有用户:返回User对象数组
func queryUsers(): Array<User> {
// RdbPredicates用于构建查询条件,这里查询users表的所有记录
let predicates = RdbPredicates("users")
// query()执行查询,返回ResultSet结果集
let resultSet = db.query(predicates)
// 创建ArrayList存储查询结果
var users = ArrayList<User>()
// 遍历结果集,goToNextRow()移动到下一行
while (resultSet.goToNextRow()) {
// 从当前行提取数据,创建User对象
let user = User(
id: resultSet.getLong("id"), // 获取id字段
name: resultSet.getString("name"), // 获取name字段
email: resultSet.getString("email"), // 获取email字段
age: resultSet.getInt("age") // 获取age字段
)
users.append(user)
}
// 关闭结果集,释放资源
resultSet.close()
// 将ArrayList转换为数组返回
return users.toArray()
}
}
六、分布式能力
分布式能力是鸿蒙的核心特色,让数据和任务在多设备间无缝流转。在待办事项 App 中,手机上添加的任务自动同步到平板和手表,在任何设备上标记完成都会实时更新到其他设备。鸿蒙提供了分布式数据同步、分布式任务调度、跨设备迁移等能力,让多设备协同变得简单。
6.1、分布式数据跨设备同步
分布式键值数据库(DistributedKVStore)自动处理数据同步,开发者只需创建实例、写入数据、监听变化,系统会自动将数据同步到同一账号下的其他设备。下面的 DistributedDataManager 封装了分布式数据操作,在初始化时监听数据变化事件,syncData() 方法写入数据并触发同步,handleDataChange() 处理来自其他设备的数据变化:
// 分布式数据管理器:实现跨设备数据同步
// 这是鸿蒙的核心特色功能,让数据在多设备间自动同步
class DistributedDataManager {
private var kvStore: DistributedKVStore // 分布式键值数据库实例
// 构造函数:初始化分布式数据库并监听数据变化
init() {
// 配置分布式数据库
let config = KVStoreConfig(
bundleName: "com.example.app", // 应用包名,用于标识应用
dataDir: "distributed_data" // 数据存储目录
)
// 获取分布式KV存储实例
this.kvStore = DistributedKVStore.getInstance(config)
// 监听数据变化事件
// 当其他设备修改数据时,会触发dataChange事件
// lambda表达式{ change => handleDataChange(change) }定义回调函数
kvStore.on("dataChange", { change => handleDataChange(change) })
}
// 同步数据到其他设备
func syncData(key: String, value: String): Unit {
// put()将数据写入本地数据库
kvStore.put(key, value)
// sync()触发跨设备同步
// 系统会自动将数据同步到同一账号下的其他设备
// 开发者无需关心网络传输、冲突解决等细节
kvStore.sync()
}
// 获取数据:从本地数据库读取
// 返回String?表示可能返回null(当key不存在时)
func getData(key: String): String? {
return kvStore.get(key)
}
// 处理数据变化事件:当其他设备修改数据时调用
private func handleDataChange(change: DataChange): Unit {
// 打印日志,方便调试
println("数据变化: ${change.key} = ${change.value}")
// 通过事件总线通知UI层数据已更新
// UI组件可以订阅DataChangedEvent来响应数据变化
EventBus.post(DataChangedEvent(change))
}
}
6.2、跨设备任务流转
class TaskMigrationManager {
// 迁移当前任务到其他设备
func migrateToDevice(deviceId: String): Bool {
try {
// 保存当前状态
let state = captureCurrentState()
// 发起迁移
let result = DistributedAbilityKit.continueAbility(
deviceId: deviceId,
bundleName: "com.example.app",
abilityName: "MainAbility",
wantParams: state
)
return result.isSuccess()
} catch (e: Exception) {
println("迁移失败: ${e.message}")
return false
}
}
// 接收迁移的任务
func onContinue(wantParams: HashMap<String, Any>): Unit {
// 恢复状态
restoreState(wantParams)
}
}
七、HTTP 网络请求封装
class HttpClient {
func get(url: String): Future<Response> {
let request = HttpRequest()
request.method = HttpMethod.GET
request.url = url
return http.request(request)
}
func post(url: String, body: String): Future<Response> {
let request = HttpRequest()
request.method = HttpMethod.POST
request.url = url
request.header("Content-Type", "application/json")
request.body = body
return http.request(request)
}
}
// 使用示例
@Component
struct DataPage {
@State private var data: Array<Item> = []
@State private var isLoading: Bool = false
func build() {
Column() {
if (isLoading) {
LoadingIndicator()
} else {
List() {
for (item in data) {
ListItem() {
Text(item.title)
}
}
}
}
}
.onAppear({
loadData()
})
}
private async func loadData(): Unit {
isLoading = true
let client = HttpClient()
let response = await client.get("https://api.example.com/items")
if (response.statusCode == 200) {
let json = JsonParser.parse(response.body)
data = json.as<Array<Item>>()
}
isLoading = false
}
}
八、应用生命周期
8.1、Ability 应用生命周期
class MainAbility <: UIAbility {
override func onCreate(want: Want, launchParam: AbilityStartSetting): Unit {
println("Ability 创建")
// 初始化应用
}
override func onWindowStageCreate(windowStage: WindowStage): Unit {
println("窗口创建")
// 加载主页面
windowStage.loadContent("pages/Index")
}
override func onForeground(): Unit {
println("应用进入前台")
// 恢复数据
}
override func onBackground(): Unit {
println("应用进入后台")
// 保存数据
}
override func onDestroy(): Unit {
println("Ability 销毁")
// 清理资源
}
}
8.2、页面组件生命周期
@Entry
@Component
struct LifecyclePage {
@State private var message: String = ""
func aboutToAppear() {
println("页面即将显示")
message = "页面已加载"
}
func onPageShow() {
println("页面显示")
}
func onPageHide() {
println("页面隐藏")
}
func aboutToDisappear() {
println("页面即将销毁")
}
func build() {
Column() {
Text(message)
}
}
}
九、系统权限申请与管理
class PermissionManager {
// 检查权限
func checkPermission(permission: String): Bool {
let result = abilityAccessCtrl.verifyAccessToken(
tokenId: getTokenId(),
permissionName: permission
)
return result == GrantStatus.PERMISSION_GRANTED
}
// 请求权限
async func requestPermission(permission: String): Bool {
if (checkPermission(permission)) {
return true
}
let result = await abilityAccessCtrl.requestPermissionsFromUser(
permissions: [permission]
)
return result.authResults[0] == GrantStatus.PERMISSION_GRANTED
}
}
// 使用示例
@Component
struct CameraPage {
private var permissionManager: PermissionManager = PermissionManager()
func build() {
Column() {
Button("打开相机")
.onClick(async () => {
let hasPermission = await permissionManager.requestPermission(
"ohos.permission.CAMERA"
)
if (hasPermission) {
openCamera()
} else {
showToast("需要相机权限")
}
})
}
}
}
十、应用发布
10.1、应用签名配置
// build-profile.json5
{
"app": {
"signingConfigs": [
{
"name": "default",
"type": "HarmonyOS",
"material": {
"certpath": "cert.pem",
"storePassword": "your_password",
"keyAlias": "your_alias",
"keyPassword": "your_password",
"profile": "profile.p7b",
"signAlg": "SHA256withECDSA",
"storeFile": "keystore.p12"
}
}
]
}
}
10.2、HAP 包构建与发布
# 构建 HAP 包
hvigorw assembleHap --mode release
# 输出位置
# build/outputs/hap/release/entry-release.hap
十一、调试技巧
11.1、调试日志与错误追踪
// 使用 hilog
import ohos.hiviewdfx.HiLog
class Logger {
private const DOMAIN: Int32 = 0x0001
private const TAG: String = "MyApp"
static func debug(message: String): Unit {
HiLog.debug(DOMAIN, TAG, message)
}
static func info(message: String): Unit {
HiLog.info(DOMAIN, TAG, message)
}
static func warn(message: String): Unit {
HiLog.warn(DOMAIN, TAG, message)
}
static func error(message: String): Unit {
HiLog.error(DOMAIN, TAG, message)
}
}
// 使用
Logger.info("应用启动")
Logger.error("发生错误: ${error.message}")
11.2、应用性能分析工具
class PerformanceTracker {
private var startTime: Int64 = 0
func start(tag: String): Unit {
startTime = Time.nanoTime()
Logger.debug("开始: ${tag}")
}
func end(tag: String): Unit {
let duration = (Time.nanoTime() - startTime) / 1_000_000
Logger.debug("结束: ${tag}, 耗时: ${duration}ms")
}
}
// 使用
let tracker = PerformanceTracker()
tracker.start("loadData")
await loadData()
tracker.end("loadData")
十二、关于作者与参考资料
12.1、作者简介
郭靖,笔名“白鹿第一帅”,大数据与大模型开发工程师,中国开发者影响力年度榜单人物。在移动应用开发和 UI 框架方面有丰富经验,对声明式 UI、状态管理、跨平台开发有深入实践。作为技术内容创作者,自 2015 年至今累计发布技术博客 300 余篇,全网粉丝超 60000+,获得 CSDN“博客专家”等多个技术社区认证,并成为互联网顶级技术公会“极星会”成员。
同时作为资深社区组织者,运营多个西南地区技术社区,包括 CSDN 成都站(10000+ 成员)、AWS User Group Chengdu、字节跳动 Trae Friends@Chengdu 等,累计组织线下技术活动超 50 场,致力于推动技术交流与开发者成长。
CSDN 博客地址:https://blog.csdn.net/qq_22695001
12.2、参考资料
- HarmonyOS 应用开发官方文档
- ArkUI 声明式开发范式
- HarmonyOS 分布式数据管理
- Flutter 开发文档(声明式 UI 参考)
- SwiftUI 教程(状态管理参考)
文章作者:白鹿第一帅,作者主页:https://blog.csdn.net/qq_22695001,未经授权,严禁转载,侵权必究!
总结
通过待办事项 App 的完整开发实践,我深刻体会到鸿蒙应用开发的技术特点和优势。ArkUI 的声明式语法让 UI 开发变得简洁高效,状态管理机制通过 @State、@Prop、@Link 等装饰器实现了清晰的数据流动,数据持久化方案从轻量级的 Preferences 到关系型数据库再到分布式数据库提供了完整的解决方案,而分布式能力更是鸿蒙的核心竞争力,让数据跨设备同步和任务迁移变得简单自然。开发过程中也遇到一些挑战,比如文档不够完善需要查看源码,社区资源相对较少需要更多探索。建议开发者循序渐进地学习,从基础组件入手,逐步掌握状态管理和数据持久化,最后探索分布式能力。随着鸿蒙生态的快速发展,现在正是入门的最佳时机。
我是白鹿,一个不懈奋斗的程序猿。望本文能对你有所裨益,欢迎大家的一键三连!若有其他问题、建议或者补充可以留言在文章下方,感谢大家的支持!
更多推荐



所有评论(0)