前言

学习仓颉语言的最终目的是开发鸿蒙原生应用。作为一名移动应用开发工程师,我通过开发待办事项 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”模板
  1. 下载 DevEco Studio(https://developer.huawei.com/consumer/cn/deveco-studio/
  2. 安装仓颉语言插件
  3. 配置 HarmonyOS SDK
  4. 创建仓颉项目

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
框架自动渲染
描述UI状态
状态变化
设置属性
创建元素
添加到DOM
监听事件
手动更新
对比项 命令式 UI 声明式 UI 优势
代码量 声明式减少 50%+
可读性 更易理解
维护性 更易维护
性能 手动优化 自动优化 框架智能优化
学习曲线 陡峭 平缓 更易上手

声明式 UI 的核心思想是描述“UI 是什么样子”而不是“如何更新 UI”,框架会自动处理 UI 的更新和渲染优化。这种编程范式让代码更简洁、更易维护,也更符合现代 UI 开发的趋势。

2.1、ArkUI 基础组件应用

基础组件
Text
Button
TextInput
Image
显示文本
样式设置
触发操作
点击事件
输入内容
实时监听
显示图标
加载图片

基础组件是构建 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、响应式布局设计

布局组件
Column
垂直布局
Row
水平布局
Stack
层叠布局
Grid
网格布局
List
列表布局
从上到下
从左到右
组件叠加
规则网格
虚拟滚动

布局是 UI 开发的核心,决定了界面的结构和视觉效果。

布局组件选择指南

垂直
水平
叠加
选择布局组件
排列方向?
Column
Row
Stack
是否规则排列?
Grid
是否长列表?
List
布局组件 使用场景 性能 复杂度
Column 垂直排列 ⭐⭐⭐⭐⭐
Row 水平排列 ⭐⭐⭐⭐⭐
Stack 组件叠加 ⭐⭐⭐⭐ ⭐⭐
Grid 网格布局 ⭐⭐⭐⭐ ⭐⭐⭐
List 长列表 ⭐⭐⭐⭐⭐ ⭐⭐

ArkUI 提供了多种布局组件:Column(垂直布局)用于从上到下排列子组件,Row(水平布局)用于从左到右排列,Stack(层叠布局)用于组件叠加显示,Grid(网格布局)用于规则的网格排列,List(列表布局)专门用于长列表的高性能渲染。

待办事项 App 布局结构

主界面
Column垂直布局
输入框区域
任务列表
TextInput
Button添加
List列表
任务项1
任务项2
任务项...
Row水平布局
Checkbox
Text任务
Button删除

在待办事项 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 框架 UI界面 修改状态 通知变化 计算差异 更新UI 自动刷新 用户操作 状态State 框架 UI界面

状态管理层次

装饰器 用途 数据流 使用场景
@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、参考资料


文章作者白鹿第一帅作者主页https://blog.csdn.net/qq_22695001,未经授权,严禁转载,侵权必究!


总结

通过待办事项 App 的完整开发实践,我深刻体会到鸿蒙应用开发的技术特点和优势。ArkUI 的声明式语法让 UI 开发变得简洁高效,状态管理机制通过 @State、@Prop、@Link 等装饰器实现了清晰的数据流动,数据持久化方案从轻量级的 Preferences 到关系型数据库再到分布式数据库提供了完整的解决方案,而分布式能力更是鸿蒙的核心竞争力,让数据跨设备同步和任务迁移变得简单自然。开发过程中也遇到一些挑战,比如文档不够完善需要查看源码,社区资源相对较少需要更多探索。建议开发者循序渐进地学习,从基础组件入手,逐步掌握状态管理和数据持久化,最后探索分布式能力。随着鸿蒙生态的快速发展,现在正是入门的最佳时机。

在这里插入图片描述


我是白鹿,一个不懈奋斗的程序猿。望本文能对你有所裨益,欢迎大家的一键三连!若有其他问题、建议或者补充可以留言在文章下方,感谢大家的支持!

Logo

数据库是今天社会发展不可缺少的重要技术,它可以把大量的信息进行有序的存储和管理,为企业的数据处理提供了强大的保障。

更多推荐