【仓颉纪元】仓颉鸿蒙应用深度开发:待办事项 App 全流程实战
文章目录
- 前言
- 一、开发环境搭建
- 1.1、DevEco Studio 环境配置
- 1.2、鸿蒙项目结构解析
- 二、ArkUI 组件开发
- 2.1、ArkUI 基础组件应用
- 2.2、响应式布局设计
- 2.3、自定义组件封装
- 三、状态管理
- 3.1、@State 本地状态管理
- 3.2、@Prop/@Link 父子组件通信
- 3.3、@Observed/@ObjectLink 复杂对象状态
- 四、页面导航
- 4.1、Router 页面跳转与参数传递
- 4.2、Tab 底部导航栏实现
- 五、数据持久化
- 5.1、Preferences 轻量级键值存储
- 5.2、关系型数据库操作
- 六、分布式能力
- 6.1、分布式数据跨设备同步
- 6.2、跨设备任务流转
- 七、HTTP 网络请求封装
- 八、应用生命周期
- 8.1、Ability 应用生命周期
- 8.2、页面组件生命周期
- 九、系统权限申请与管理
- 十、应用发布
- 10.1、应用签名配置
- 10.2、HAP 包构建与发布
- 十一、调试技巧
- 11.1、调试日志与错误追踪
- 11.2、应用性能分析工具
- 十二、关于作者与参考资料
- 12.1、作者简介
- 12.2、参考资料
- 总结
前言
学习仓颉语言的最终目的是开发鸿蒙原生应用。作为一名移动应用开发工程师,我通过开发待办事项 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 = 0func build() {// Column的spacing参数设置子组件之间的垂直间距为10vpColumn(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 设置子组件之间的垂直间距为20vpColumn(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 设置子组件之间的水平间距为10vpRow(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循环遍历数据源,为每个数据项创建ListItemfor (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 // 头像URLprivate var bio: String // 个人简介private var onTap: () -> Unit // 点击回调函数// 构造函数:创建组件实例时初始化属性public init(name: String, avatar: String, bio: String, onTap: () -> Unit) {this.name = namethis.avatar = avatarthis.bio = biothis.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, // 传递头像URLbio: 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自减1Button("减少").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: Stringfunc build() { Text("接收到: ${text}") // 显示父组件传入的值}
}// 子组件2:使用@Link绑定数据(可读写)
@Component
struct ChildComponent2 {// @Link表示这是与父组件双向绑定的引用// 子组件修改text时,父组件的message也会同步更新@Link private var text: Stringfunc 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 = idthis.title = titlethis.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变trueitem.completed = !item.completedbreak // 找到后立即退出循环,提高性能}}}// 删除任务:使用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 = itemthis.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 Int64name = 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 = 0func 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中读取深色模式设置,如果不存在则返回falsedarkMode = 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) => { // 保存深色模式设置到Preferencesprefs.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)}// 插入新用户:返回新插入记录的IDfunc 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()方法插入数据,返回新记录的IDreturn 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.GETrequest.url = urlreturn http.request(request)}func post(url: String, body: String): Future<Response> {let request = HttpRequest()request.method = HttpMethod.POSTrequest.url = urlrequest.header("Content-Type", "application/json")request.body = bodyreturn http.request(request)}
}// 使用示例
@Component
struct DataPage {@State private var data: Array<Item> = []@State private var isLoading: Bool = falsefunc build() {Column() {if (isLoading) {LoadingIndicator()} else {List() {for (item in data) {ListItem() {Text(item.title)}}}}}.onAppear({loadData()})}private async func loadData(): Unit {isLoading = truelet 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.HiLogclass Logger {private const DOMAIN: Int32 = 0x0001private 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 = 0func start(tag: String): Unit {startTime = Time.nanoTime()Logger.debug("开始: ${tag}")}func end(tag: String): Unit {let duration = (Time.nanoTime() - startTime) / 1_000_000Logger.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 到关系型数据库再到分布式数据库提供了完整的解决方案,而分布式能力更是鸿蒙的核心竞争力,让数据跨设备同步和任务迁移变得简单自然。开发过程中也遇到一些挑战,比如文档不够完善需要查看源码,社区资源相对较少需要更多探索。建议开发者循序渐进地学习,从基础组件入手,逐步掌握状态管理和数据持久化,最后探索分布式能力。随着鸿蒙生态的快速发展,现在正是入门的最佳时机。
我是白鹿,一个不懈奋斗的程序猿。望本文能对你有所裨益,欢迎大家的一键三连!若有其他问题、建议或者补充可以留言在文章下方,感谢大家的支持!
