鸿蒙待办事项 App 开发实战 — 从零搭建到运行
一、前言
在前两篇文章中,我们完成了鸿蒙计算器的完整开发,已经掌握了 ArkUI 页面布局、状态管理、按钮事件处理等核心技能。今天我们将这些技能应用到另一个经典项目——待办事项应用(Todo List)。
待办事项 App 和计算器一样,是移动端最经典的入门项目之一。但相比于计算器,它覆盖了 ArkUI 开发中更多重要的知识点:输入框组件、列表渲染、增删改查操作、以及数据持久化。如果说计算器让你学会了"怎么处理用户点击",那么待办 App 将让你学会"怎么管理用户数据"。
为什么选择待办 App 作为第二个项目?因为它是绝大多数人手机上都会安装的工具类应用,功能明确、需求清晰,而且增删改查是几乎所有应用(通讯录、备忘录、购物清单)的基础模式。做完这个项目,你可以轻松举一反三,开发出各种列表型应用。
二、项目准备
2.1 开发环境要求
- IDE:DevEco Studio(推荐最新版本)
- SDK:API 23 或以上,在 SDK Manager 中确认已安装
- 语言/框架:ArkTS + ArkUI
2.2 新建项目
打开 DevEco Studio,按以下步骤操作:
- 点击 File → New → Create Project
- 选择 Empty Ability 模板
- 填写项目信息:
| 配置项 | 推荐值 |
|---|---|
| 项目名称 | TodoList |
| 包名 | com.example.todolist |
| Compile SDK | API 23 |
| 模块名称 | entry |
- 点击 Finish 完成创建
创建完成后等待 IDE 自动同步。同步成功后,可以看到标准的 HarmonyOS 项目结构,我们只需要修改 entry/src/main/ets/pages/Index.ets 这一个文件。
三、功能设计
3.1 界面布局
待办事项 App 采用经典的分层结构,从上到下依次是:标题栏、输入区、列表区、底部统计栏:
┌─────────────────────────────┐
│ │
│ ✅ 今日待办 (3/5) │ ← 顶部标题 + 实时进度
│ │
├─────────────────────────────┤
│ ┌───────────────────────┐ │
│ │ 输入新待办... ➕ │ │ ← TextInput + 添加按钮
│ └───────────────────────┘ │
├─────────────────────────────┤
│ ☐ 买牛奶 │ ← 未完成(空心圆)
│ ☑ 交电费 │ ← 已完成(✅ + 删除线)
│ ☐ 写周报 │
│ ☐ 健身 │
│ │
├─────────────────────────────┤
│ 共 4 项 清除已完成 │ ← 底部统计 + 操作
└─────────────────────────────┘
3.2 功能清单
| 功能 | 详细说明 |
|---|---|
| 添加待办 | 在输入框输入文本,点击 ➕ 按钮或按回车添加 |
| 标记完成 | 点击左侧 ○/✅ 切换待办的完成状态 |
| 删除待办 | 点击右侧 🗑️ 按钮移除指定待办 |
| 实时统计 | 顶部显示"已完成数 / 总数",底部显示"共 N 项" |
| 清除已完成 | 一键删除所有已完成项,只保留未完成的 |
| 空输入保护 | 输入为空或只有空格时不添加 |
| 数据持久化 | 关闭 App 后数据不会丢失,下次打开自动恢复 |
四、完整代码实现
4.1 数据模型
首先定义待办事项的数据结构:
interface TodoItem {
id: number // 唯一标识,用于区分和操作每条待办
text: string // 待办文本内容
done: boolean // 完成状态:false=未完成,true=已完成
}
在 ArkTS 中使用 interface 定义数据类型,可以确保后续操作中字段名的一致性,避免拼写错误。
4.2 状态变量
@State todos: TodoItem[] = [] // 全部待办列表(响应式)
@State inputText: string = '' // 输入框文本(响应式)
@StorageLink('todos_data') savedTodos: string = '[]' // 持久化
private nextId: number = 1 // 下一个可用的 ID(非 UI 状态)
这里我们用到了一种新的装饰器:@StorageLink。它的作用是将变量与设备的本地存储自动同步——变量赋值时自动写入存储,App 启动时自动读取恢复。字符串参数 'todos_data' 是存储键名,需要保证唯一性。
对比一下 @State 和 @StorageLink 的区别:
| 装饰器 | 生命周期 | 用途 |
|---|---|---|
@State |
页面存续期间 | 临时 UI 状态,刷新即失 |
@StorageLink |
设备存续期间 | 用户数据,关闭 App 还在 |
4.3 添加待办
private addTodo(): void {
const text = this.inputText.trim()
if (text === '') return // 空内容不添加
const newItem: TodoItem = {
id: this.nextId++, // 自增 ID,保证唯一
text: text,
done: false
}
this.todos = [newItem].concat(this.todos) // 新项插入最前面
this.inputText = '' // 清空输入框
}
为什么不用 this.todos.unshift(newItem) 呢?因为 @State 检测的是变量引用是否变化——unshift 修改的是原数组,引用不变,ArkUI 不会触发渲染。而 concat 返回新数组,引用变了,UI 自动更新。
注意这里没用 [newItem, ...this.todos] 的展开运算符写法,因为 ArkTS 编译器禁止对数组使用展开语法(规则 arkts-no-spread)。改用 concat 达到同样效果。
4.4 切换完成状态
private toggleTodo(id: number): void {
const newTodos: TodoItem[] = []
for (let i = 0; i < this.todos.length; i++) {
const item = this.todos[i]
if (item.id === id) {
// 创建新对象,改变 done 状态
newTodos.push({ id: item.id, text: item.text, done: !item.done })
} else {
newTodos.push(item)
}
}
this.todos = newTodos
}
ArkTS 不支持 map 和数组展开运算符 {...item},所以这里用 for 循环手动构建新数组。需要修改的那一项创建一个新的对象,其他项直接引用原对象,既保证了不可变性,又避免了不必要的对象创建。
4.5 删除待办
private deleteTodo(id: number): void {
const newTodos: TodoItem[] = []
for (let i = 0; i < this.todos.length; i++) {
if (this.todos[i].id !== id) {
newTodos.push(this.todos[i])
}
}
this.todos = newTodos
}
ArkTS 不支持 filter,用 for 循环 + if 跳过要删除的项,实现同样的过滤效果。
4.6 清除已完成
private clearDone(): void {
const newTodos: TodoItem[] = []
for (let i = 0; i < this.todos.length; i++) {
if (!this.todos[i].done) {
newTodos.push(this.todos[i])
}
}
this.todos = newTodos
}
4.7 数据持久化
使用 @StorageLink 后,持久化的读写变得非常简单:
private saveTodos(): void {
this.savedTodos = JSON.stringify(this.todos)
}
private loadTodos(): void {
if (this.savedTodos && this.savedTodos !== '[]') {
this.todos = JSON.parse(this.savedTodos)
// 恢复 nextId:取最大 ID + 1
let maxId = 0
for (let i = 0; i < this.todos.length; i++) {
if (this.todos[i].id > maxId) {
maxId = this.todos[i].id
}
}
this.nextId = maxId + 1
}
}
aboutToAppear(): void {
this.loadTodos()
}
aboutToAppear 是 ArkUI 的生命周期回调函数,在页面初始化完成后、UI 渲染之前自动调用。我们在这里读取之前保存的数据,恢复到 todos 数组中。
数据恢复后还要同步 nextId。假如用户之前添加了 5 条待办(ID 1-5),删除 3 条后剩下 ID 为 1、3、5 的三条。如果不恢复 nextId,下次添加时会从 1 开始,和已有的 ID 冲突。所以我们遍历现有数据找到最大 ID,加 1 作为下一个可用 ID。
4.8 计算属性
get completedCount(): number {
return this.todos.filter(item => item.done).length
}
get totalCount(): number {
return this.todos.length
}
get hasCompleted(): boolean {
return this.completedCount > 0
}
计算属性(getter)是 ArkTS 中非常实用的功能,它们像普通属性一样使用(this.completedCount),但实际上每次访问都会重新计算。因为 todos 是 @State 变量,当它变化时,所有依赖它的 getter 都会自动重新求值,UI 随之刷新。
4.9 完整 UI 构建
build() {
Column() {
// ═══ 顶部标题栏 ═══
Row() {
Text('✅ 今日待办')
.fontSize(26).fontWeight(FontWeight.Bold).fontColor(Color.White)
Blank()
Text(`${this.completedCount} / ${this.totalCount}`)
.fontSize(18).fontColor('#8E8E93')
}
.width('100%').padding(20)
// ═══ 输入区域 ═══
Row() {
TextInput({ placeholder: '输入新待办...', text: this.inputText })
.layoutWeight(1)
.height(48).backgroundColor('#2C2C2E').borderRadius(12)
.padding({ left: 16 }).fontColor(Color.White)
.placeholderColor('#8E8E93')
.onChange((value: string) => { this.inputText = value })
.onSubmit(() => { this.addTodo() }) // 回车提交
Button('➕')
.fontSize(22).width(48).height(48).borderRadius(24)
.backgroundColor('#FF9F0A').margin({ left: 8 })
.onClick(() => { this.addTodo() })
}
.width('100%').padding({ left: 20, right: 20 })
// ═══ 待办列表 ═══
List({ space: 8 }) {
ForEach(this.todos, (item: TodoItem) => {
ListItem() {
Row() {
// 勾选按钮:未完成显示 ⭕,已完成显示 ✅
Button(item.done ? '✅' : '⭕')
.fontSize(18).width(36).height(36)
.backgroundColor(Color.Transparent)
.onClick(() => { this.toggleTodo(item.id) })
// 待办文本:已完成加删除线
Text(item.text)
.fontSize(18)
.fontColor(item.done ? '#555555' : Color.White)
.decoration({
type: item.done
? TextDecorationType.LineThrough
: TextDecorationType.None
})
.layoutWeight(1).padding({ left: 8 })
// 删除按钮
Button('🗑️')
.fontSize(16).backgroundColor(Color.Transparent)
.onClick(() => { this.deleteTodo(item.id) })
}
.width('100%').padding(12)
.backgroundColor('#1C1C1E').borderRadius(12)
}
}, (item: TodoItem) => item.id.toString()) // 唯一 key
}
.layoutWeight(1).width('100%')
.padding({ left: 20, right: 20 })
// ═══ 底部统计栏 ═══
Row() {
Text(`共 ${this.totalCount} 项`)
.fontSize(14).fontColor('#8E8E93')
Blank()
if (this.hasCompleted) {
Text('清除已完成')
.fontSize(14).fontColor('#FF9F0A')
.onClick(() => { this.clearDone() })
}
}
.width('100%').padding(20)
.backgroundColor('#1C1C1E')
.borderRadius({ topLeft: 16, topRight: 16 })
}
.width('100%').height('100%').backgroundColor('#000000')
}
UI 中的几个关键组件说明:
TextInput:ArkUI 的文本输入框组件。placeholder设置占位提示文字,onChange监听输入变化,onSubmit监听键盘回车事件List+ListItem:高效的虚拟滚动列表,只渲染可见区域的列表项,数据量大时也不会卡顿ForEach的 key:(item: TodoItem) => item.id.toString()为每个列表项提供唯一标识,帮助 ArkUI 精确追踪组件变化decoration:Text 组件的装饰属性,这里用LineThrough实现已完成事项的删除线效果Blank():弹性占位组件,将左右两侧的元素推开
4.10 为什么每次都要创建新数组
初学 ArkTS 的开发者可能会问:为什么增删改都要创建新数组?直接用 this.todos.push() 或 this.todos.splice() 不是更简单吗?
原因在于 ArkUI 的变更检测机制。@State 装饰器通过引用比较来判断一个变量是否发生了变化:
// ❌ 不触发 UI 更新
this.todos.push(newItem) // 数组引用没变,UI 不变
// ✅ 触发 UI 更新
this.todos = [newItem].concat(this.todos) // 新数组,新引用,UI 刷新
这就是为什么我们的增删改操作都创建新数组:concat 连接、for 循环构建新数组。这个模式叫做不可变更新(Immutable Update),在 React、Vue 等现代前端框架中也非常常见。
4.11 保存时机优化
每次增删改后立即保存可能会频繁写入存储,但 @StorageLink 底层做了异步批量处理,对性能影响很小。如果要进一步优化,可以在 aboutToDisappear 时一次性保存:
aboutToDisappear(): void {
this.savedTodos = JSON.stringify(this.todos)
}
这样只有离开页面时才写入一次。但缺点是如果 App 被系统强杀(比如耗完电池自动关机),最后的状态可能来不及保存。权衡之下,每次操作后立即保存是更稳妥的做法。
五、运行与测试
5.1 替换代码
打开 entry/src/main/ets/pages/Index.ets,全选替换为上面提供的完整代码。IDE 会自动编译,等待几秒即可。
5.2 启动模拟器
点击右侧 Device Manager,创建并启动手机模拟器。首次启动可能需要 2-5 分钟。
5.3 功能测试
| # | 测试操作 | 预期结果 |
|---|---|---|
| 1 | 输入"买牛奶" → 点 ➕ | 列表显示"买牛奶",顶部变为 0/1 |
| 2 | 直接点 ➕(不输入) | 无反应,空输入保护生效 |
| 3 | 输入" "(空格) → 点 ➕ | 无反应,trim() 后为空 |
| 4 | 点 ⭕ 切换完成 | 变为 ✅,文字加删除线,顶部 1/1 |
| 5 | 再点 ✅ | 恢复为 ⭕,删除线消失,顶部 0/1 |
| 6 | 添加多条,点 🗑️ 删除 | 该条消失,统计数字减少 |
| 7 | 标记 2 条完成 → 清除已完成 | 已完成消失,未完成保留 |
| 8 | 添加 5 条,退出 App 重开 | 所有数据还在 |
| 9 | 键盘回车添加 | 和点 ➕ 效果一样 |
| 10 | 连续快速添加 | 每条都正常显示,ID 不重复 |
六、运行效果

通过以上完整的开发过程可以发现,一个功能全面的待办 App 其实只需要不到 200 行代码就能实现。这得益于 ArkUI 声明式框架的高效表达力——用最少的代码描述最多的界面和交互逻辑。
七、扩展思路
一个基础版待办 App 做完后,可以通过以下方向继续深化:
- 分类标签:增加"工作 💼 / 生活 🏠 / 学习 📚"分类,按标签筛选显示
- 优先级:高/中/低三级,高优先级用红色标记
- 截止日期:集成 DatePicker 选择截止日期,超时自动变红
- 编辑功能:点击待办文本弹出编辑框,支持修改内容
- 排序拖拽:长按拖拽调整待办顺序
- 搜索功能:增加搜索框,实时过滤待办列表
每个扩展方向都对应了 ArkUI 的一个新知识点,比如 DatePicker 日期组件、动画 API、搜索过滤等。
八、总结
本文从零开始完成了鸿蒙待办事项 App 的开发,核心知识点回顾:
- ✅ 数据模型:使用
interface定义清楚的 TodoItem 结构 - ✅ 状态管理:
@State驱动 UI,@StorageLink持久化保存 - ✅ 数组不可变性:用
concat、for循环创建新数组而非直接修改原数组 - ✅ 列表渲染:
List+ListItem+ForEach实现高效列表 - ✅ 生命周期:
aboutToAppear初始化数据,aboutToDisappear善后 - ✅ getter 计算属性:
completedCount、totalCount实时计算 - ✅ 条件渲染:只有有完成项时才显示"清除已完成"按钮
- ✅ 条件样式:已完成文字加删除线,颜色变灰
待办 App 是鸿蒙开发入门阶段非常值得动手实践的项目。它比计算器更贴近实际应用场景,代码量适中,但覆盖的知识面更广。掌握了计算器和待办这两个项目,你已经具备了独立开发简单工具类鸿蒙应用的能力。
更多推荐

所有评论(0)