HarmonyOS实战记录:从零开发待办事项应用
📋 项目信息
| 项目信息 | 内容 |
|---|---|
| 项目名称 | 待办事项应用(TodoList) |
| 开发工具 | DevEco Studio 5.0 |
| 开发语言 | ArkTS |
| API版本 | 23 |
| 项目类型 | Application |
| 包名 | com.example.todolist |
| 开发时间 | 约6小时(包含调试) |
第一部分:需求分析与技术选型
1.1 需求分析
1.1.1 核心功能
在我开始写代码之前,我先明确了这个待办事项应用需要哪些功能:
基础功能(必须有):
- ✅ 添加新的待办事项
- ✅ 标记待办事项为已完成
- ✅ 删除待办事项
- ✅ 显示待办事项列表
进阶功能(可以有):
- ✅ 统计已完成/总数
- ✅ 一键清除已完成事项
- ✅ 数据持久化(应用关闭后数据不丢失)
可选功能(暂时不做):
- ❌ 待办事项分类(工作/生活/学习)
- ❌ 设置提醒时间
- ❌ 云同步
- ❌ 语音输入
1.1.2 用户体验设计
我参考了iOS的"提醒事项"应用和Android的"Google Tasks"应用,确定了以下设计原则:
- 简洁直观:界面不能太复杂,一眼就能看懂
- 操作流畅:添加、完成、删除操作不能超过2次点击
- 视觉反馈:完成状态要有明确的视觉区分(比如删除线)
- 数据持久化:关闭应用后再打开,数据还在
1.2 技术选型
1.2.1 数据存储方案对比
在HarmonyOS中,有多种数据存储方案可以选择:
| 存储方案 | 优点 | 缺点 | 适用场景 | 我的选择 |
|---|---|---|---|---|
| @StorageLink | 1. API简单 2. 自动持久化 3. 支持复杂对象 |
1. 性能一般 2. 不适合大量数据 |
✅ 小型应用 ✅ 配置数据 |
✅ 选择这个 |
| Preferences | 1. 轻量级 2. 性能好 |
1. 只支持基本类型 2. 不支持复杂对象 |
✅ 用户设置 ❌ 不适合待办事项 |
❌ 不选择 |
| 关系型数据库 | 1. 支持复杂查询 2. 性能好 |
1. API复杂 2. 需要建表 |
❌ 大型应用 ❌ 不适合简单待办 |
❌ 不选择 |
| 分布式数据管理 | 1. 支持跨设备同步 | 1. 非常复杂 2. 需要华为账号 |
❌ 云同步场景 | ❌ 不选择 |
最终选择:@StorageLink
理由:
- API非常简单(类似LocalStorage)
- 自动持久化(不需要手动调用保存方法)
- 支持存储复杂对象(数组、对象)
- 对于待办事项这种小型应用,性能完全够用
1.2.2 UI框架选择
HarmonyOS提供了多种UI开发框架:
| UI框架 | 特点 | 我的选择 |
|---|---|---|
| ArkUI (Declarative) | 1. 声明式UI 2. 响应式数据绑定 3. 组件化 |
✅ 选择这个 |
| ArkUI (Classic) | 1. 命令式UI 2. 类似Android View系统 |
❌ 不选择 |
| Java UI | 1. 传统命令式UI 2. 已被废弃 |
❌ 不选择 |
最终选择:ArkUI (Declarative)
理由:
- 声明式UI开发效率更高
- 响应式数据绑定,代码更简洁
- 官方主推,未来会得到更好支持
第二部分:项目搭建与基础结构设计
2.1 创建项目
步骤1:打开DevEco Studio
(假设你已经安装好了DevEco Studio,这里不再赘述环境搭建过程)
步骤2:新建项目
- 点击 “New Project”
- 选择 “Empty Ability” 模板
- 点击 “Next”
步骤3:配置项目信息
Project Name: TodoList
Bundle Name: com.example.todolist
Save Location: E:\HMproject\Project\TodoList
Compile SDK: API 23
点击 “Finish”,等待项目创建完成。
2.2 定义数据结构
在 entry/src/main/ets/pages/Index.ets 文件中,我首先定义了待办事项的数据结构:
interface TodoItem {
id: number // 唯一标识符
text: string // 待办事项内容
done: boolean // 是否已完成
}
为什么需要 id 字段?
一开始我其实没有设计 id 字段,而是用数组索引来标识每一个待办事项。但是后来发现了一个问题:
问题场景:
待办事项列表:
索引0: { text: "学习HarmonyOS", done: false }
索引1: { text: "写博客", done: false }
索引2: { text: "跑步", done: false }
如果我删除索引1的待办事项("写博客"),
那么原来的索引2("跑步")会变成新的索引1。
这时候如果用户快速点击删除按钮,可能会删错 item!
解决方案:给每个待办事项分配一个唯一的 id,用 id 来标识待办事项,而不是用数组索引。
id 的生成策略:
我使用了一个简单的自增ID策略:
- 每新增一个待办事项,
nextId加1 nextId从1开始
private nextId: number = 1
private addTodo(): void {
const newItem: TodoItem = {
id: this.nextId++, // 使用当前nextId,然后自增
text: text,
done: false
}
// ...
}
为什么不用时间戳作为 id?
我考虑过用 Date.now() 作为 id,但是有以下问题:
- 不够直观:调试时看到
id: 1710746132000,不知道是第几个待办事项 - 可能重复:如果在同一毫秒内添加两个待办事项(虽然概率很低),会
id冲突
所以最终选择了自增ID策略。
2.3 设计状态管理
在ArkUI中,状态管理是非常核心的概念。我需要管理以下状态:
@Entry
@Component
struct Index {
@State todos: TodoItem[] = [] // 待办事项列表
@State inputText: string = '' // 输入框的文本
@StorageLink('todos_data') savedTodos: string = '[]' // 持久化存储
private nextId: number = 1 // 下一个待办事项的ID
// ...
}
状态变量详解:
| 状态变量 | 装饰器 | 作用 | 为什么用这个装饰器 |
|---|---|---|---|
todos |
@State |
待办事项列表 | UI需要响应这个状态的变化(添加/删除/修改待办事项时,UI要更新) |
inputText |
@State |
输入框的文本 | UI需要响应这个状态的变化(用户输入时,输入框要实时显示) |
savedTodos |
@StorageLink |
持久化存储 | 需要将数据持久化到本地,并且数据变化时自动保存 |
nextId |
无装饰器 | 下一个待办事项的ID | 这个值变化不需要驱动UI更新,所以不需要装饰器 |
@State vs @StorageLink:
| 装饰器 | 作用 | 数据持久化 | 适用场景 |
|---|---|---|---|
@State |
组件内部状态,变化时会驱动UI更新 | ❌ 不持久化 | ✅ 临时状态 ✅ UI相关的状态 |
@StorageLink |
与持久化存储双向绑定,变化时会驱动UI更新 | ✅ 自动持久化 | ✅ 需要持久化的状态 ✅ 跨页面共享的状态 |
我的用法:
todos用@State:因为todos的变化需要立即驱动UI更新savedTodos用@StorageLink:因为需要将todos序列化后存储到本地
两者配合工作:
- 当用户添加/删除/修改待办事项时,
todos变化,UI更新 - 同时,我手动将
todos序列化成JSON字符串,保存到savedTodos savedTodos会自动持久化到本地存储- 应用下次启动时,从
savedTodos读取数据,反序列化成todos
第三部分:核心功能实现(附完整代码与详解)
3.1 添加待办事项
3.1.1 功能描述
用户可以在输入框中输入待办事项的内容,然后点击"➕"按钮或按回车键,将待办事项添加到列表中。
交互细节:
- 输入框为空时,点击"➕"按钮无反应
- 输入框有内容时,点击"➕"按钮或按回车键,将待办事项添加到列表顶部
- 添加成功后,清空输入框
3.1.2 代码实现
private addTodo(): void {
const text = this.inputText.trim()
if (text === '') return
const newItem: TodoItem = {
id: this.nextId++,
text: text,
done: false
}
this.todos = [newItem].concat(this.todos)
this.inputText = ''
this.saveTodos()
}
3.1.3 代码详解
第1-3行:输入校验
const text = this.inputText.trim()
if (text === '') return
-
this.inputText.trim():- 去掉字符串首尾的空格
- 示例:
" 学习HarmonyOS "→"学习HarmonyOS" - 为什么要
trim()?- 如果用户输入了全空格(比如误触),不应该添加为空待办事项
- 如果用户输入了前后空格,应该自动去掉
-
if (text === '') return:- 如果去掉空格后为空字符串,直接返回,不添加待办事项
第5-9行:创建新的待办事项
const newItem: TodoItem = {
id: this.nextId++,
text: text,
done: false
}
-
const newItem: TodoItem:- 创建一个
TodoItem对象 const表示这个引用不能被修改(但是对象的属性可以被修改)
- 创建一个
-
id: this.nextId++:- 使用当前的
nextId作为id - 然后
nextId自增1(因为用了++后缀运算符) - 示例:
- 如果
nextId是1,那么id是1,然后nextId变成2 - 如果
nextId是2,那么id是2,然后nextId变成3
- 如果
- 使用当前的
-
done: false:- 新添加的待办事项,默认是未完成状态
第11行:将新待办事项添加到列表顶部
this.todos = [newItem].concat(this.todos)
-
[newItem]:- 创建一个只包含
newItem的数组
- 创建一个只包含
-
.concat(this.todos):- 将原来的
this.todos数组拼接到后面 - 返回一个新的数组
- 将原来的
-
为什么不用
this.todos.unshift(newItem)?- 因为
this.todos是用@State装饰的 @State装饰的数组,必须通过重新赋值来触发UI更新- 如果直接调用
unshift(),不会触发UI更新
- 因为
-
为什么不这样写?
this.todos.unshift(newItem) // ❌ 错误!不会触发UI更新 -
正确的写法:
this.todos = [newItem].concat(this.todos) // ✅ 正确!会触发UI更新
第13行:清空输入框
this.inputText = ''
- 添加成功后,清空输入框
- 因为
inputText是用@State装饰的,所以UI会自动更新(输入框会显示为空)
第14行:保存待办事项到本地存储
this.saveTodos()
- 调用
saveTodos()方法,将todos序列化后保存到savedTodos - 具体实现见后面的
saveTodos()方法详解
3.1.4 输入框的事件绑定
为了提升用户体验,我不仅绑定了按钮的点击事件,还绑定了输入框的 Submit 事件(用户按回车键时触发):
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() }) // 绑定回车事件
.onChange() 事件:
.onChange((value: string) => { this.inputText = value })
- 当输入框的内容变化时,会触发这个回调
value是最新的输入内容- 我将其保存到
this.inputText中,这样就可以在addTodo()方法中获取输入内容
.onSubmit() 事件:
.onSubmit(() => { this.addTodo() })
- 当用户在输入框中按回车键时,会触发这个回调
- 我调用
this.addTodo()方法,添加待办事项 - 这提升了用户体验:用户不需要点击"➕"按钮,直接按回车就可以了
3.2 标记待办事项为已完成
3.2.1 功能描述
用户可以点击待办事项前面的图标(⭕或✅),来切换待办事项的完成状态。
交互细节:
- 未完成的待办事项,前面显示 ⭕
- 已完成的待办事项,前面显示 ✅
- 点击图标后,切换完成状态
- 已完成的待办事项,文字会有删除线,并且颜色变灰
3.2.2 代码实现
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) {
newTodos.push({ id: item.id, text: item.text, done: !item.done })
} else {
newTodos.push(item)
}
}
this.todos = newTodos
this.saveTodos()
}
3.2.3 代码详解
为什么不用 Array.map()?
其实我一开始是用 Array.map() 来实现的:
// 第一版实现(有bug)
private toggleTodo(id: number): void {
this.todos = this.todos.map(item => {
if (item.id === id) {
return { id: item.id, text: item.text, done: !item.done }
}
return item
})
this.saveTodos()
}
但是后来发现了一个问题:ESLint报错!
error Insert `⏎` prettier/prettier
这个错误是因为 Prettier 代码格式化工具要求箭头函数的函数体如果超过一定长度,必须换行。
解决方案:改用 for 循环(也更易于调试)。
第2行:创建新的数组
const newTodos: TodoItem[] = []
- 我创建了一个新的数组
newTodos - 为什么要创建新的数组?
- 因为
this.todos是用@State装饰的 - 必须通过重新赋值来触发UI更新
- 如果直接修改数组中的对象,不会触发UI更新
- 因为
第3-11行:遍历原数组,找到目标待办事项
for (let i = 0; i < this.todos.length; i++) {
const item = this.todos[i]
if (item.id === id) {
newTodos.push({ id: item.id, text: item.text, done: !item.done })
} else {
newTodos.push(item)
}
}
-
遍历原数组:
- 用
for循环遍历this.todos数组
- 用
-
判断是否是目标待办事项:
- 如果
item.id === id,说明这就是用户点击的那个待办事项 - 需要切换它的
done状态
- 如果
-
切换
done状态:done: !item.done!item.done表示取反- 如果原来是
false(未完成),变成true(已完成) - 如果原来是
true(已完成),变成false(未完成)
-
推入新数组:
- 无论是修改了的待办事项,还是未修改的待办事项,都推入
newTodos数组
- 无论是修改了的待办事项,还是未修改的待办事项,都推入
第13行:重新赋值给 this.todos
this.todos = newTodos
- 将新数组赋值给
this.todos - 因为
this.todos是用@State装饰的,所以会触发UI更新
第14行:保存待办事项到本地存储
this.saveTodos()
- 调用
saveTodos()方法,保存数据
3.2.4 UI中的绑定
在UI中,我是这样绑定 toggleTodo() 方法的:
Button(item.done ? '✅' : '⭕')
.fontSize(18).width(36).height(36)
.backgroundColor(Color.Transparent)
.onClick(() => { this.toggleTodo(item.id) })
-
item.done ? '✅' : '⭕':- 根据
item.done的值,决定显示哪个图标 - 如果
done是true,显示 ✅ - 如果
done是false,显示 ⭕
- 根据
-
.onClick(() => { this.toggleTodo(item.id) }):- 绑定点击事件
- 点击时,调用
this.toggleTodo(item.id),并传入待办事项的id
3.3 删除待办事项
3.3.1 功能描述
用户可以点击待办事项右边的删除按钮(🗑️),来删除待办事项。
交互细节:
- 点击删除按钮后,待办事项立即消失
- 不需要确认对话框(提升操作效率)
3.3.2 代码实现
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
this.saveTodos()
}
3.3.3 代码详解
核心思路:遍历原数组,将不是目标待办事项的元素推入新数组。
第2行:创建新的数组
const newTodos: TodoItem[] = []
- 创建一个新的数组
newTodos
第3-7行:遍历原数组,过滤掉目标待办事项
for (let i = 0; i < this.todos.length; i++) {
if (this.todos[i].id !== id) {
newTodos.push(this.todos[i])
}
}
-
遍历原数组:
- 用
for循环遍历this.todos数组
- 用
-
判断是否是目标待办事项:
- 如果
this.todos[i].id !== id,说明这不是用户要删除的待办事项 - 将其推入
newTodos数组
- 如果
-
如果
this.todos[i].id === id:- 说明这就是用户要删除的待办事项
- 不推入
newTodos数组(相当于过滤掉了)
第9行:重新赋值给 this.todos
this.todos = newTodos
- 将新数组赋值给
this.todos - 触发UI更新
第10行:保存待办事项到本地存储
this.saveTodos()
- 调用
saveTodos()方法,保存数据
3.3.4 UI中的绑定
在UI中,我是这样绑定 deleteTodo() 方法的:
Button('🗑️')
.fontSize(16).backgroundColor(Color.Transparent)
.onClick(() => { this.deleteTodo(item.id) })
.onClick(() => { this.deleteTodo(item.id) }):- 绑定点击事件
- 点击时,调用
this.deleteTodo(item.id),并传入待办事项的id
3.4 清除所有已完成的待办事项
3.4.1 功能描述
用户可以点击底部的"清除已完成"按钮,一键删除所有已完成的待办事项。
交互细节:
- 只有当有已完成的待办事项时,"清除已完成"按钮才显示
- 点击后,所有已完成的待办事项被删除
- 未完成的待办事项不受影响
3.4.2 代码实现
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
this.saveTodos()
}
3.4.3 代码详解
核心思路:遍历原数组,将未完成的待办事项推入新数组。
第2行:创建新的数组
const newTodos: TodoItem[] = []
- 创建一个新的数组
newTodos
第3-7行:遍历原数组,过滤掉已完成的待办事项
for (let i = 0; i < this.todos.length; i++) {
if (!this.todos[i].done) {
newTodos.push(this.todos[i])
}
}
-
遍历原数组:
- 用
for循环遍历this.todos数组
- 用
-
判断是否是未完成的待办事项:
- 如果
!this.todos[i].done(即done是false),说明这是未完成的待办事项 - 将其推入
newTodos数组
- 如果
-
如果
this.todos[i].done是true:- 说明这是已完成的待办事项
- 不推入
newTodos数组(相当于过滤掉了)
第9行:重新赋值给 this.todos
this.todos = newTodos
- 将新数组赋值给
this.todos - 触发UI更新
第10行:保存待办事项到本地存储
this.saveTodos()
- 调用
saveTodos()方法,保存数据
3.4.4 UI中的绑定
在UI中,我是这样绑定 clearDone() 方法的:
if (this.hasCompleted) {
Text('清除已完成')
.fontSize(14).fontColor('#FF9F0A')
.onClick(() => { this.clearDone() })
}
-
if (this.hasCompleted):- 只有当有已完成的待办事项时,才显示"清除已完成"按钮
this.hasCompleted是一个计算属性(后面会详解)
-
.onClick(() => { this.clearDone() }):- 绑定点击事件
- 点击时,调用
this.clearDone()方法
3.5 数据持久化
3.5.1 功能描述
应用需要将待办事项数据持久化到本地存储,这样用户关闭应用后再打开,数据还在。
交互细节:
- 每次添加/删除/修改待办事项时,自动保存到本地存储
- 应用启动时,自动从本地存储读取数据
3.5.2 保存数据
private saveTodos(): void {
this.savedTodos = JSON.stringify(this.todos)
}
代码详解:
-
JSON.stringify(this.todos):- 将
this.todos数组序列化成JSON字符串 - 示例:
this.todos = [ { id: 1, text: '学习HarmonyOS', done: false }, { id: 2, text: '写博客', done: true } ] JSON.stringify(this.todos) // 返回: // '[{"id":1,"text":"学习HarmonyOS","done":false},{"id":2,"text":"写博客","done":true}]'
- 将
-
this.savedTodos = ...:- 将JSON字符串赋值给
this.savedTodos - 因为
savedTodos是用@StorageLink('todos_data')装饰的 - 所以会自动持久化到本地存储(key是
'todos_data')
- 将JSON字符串赋值给
3.5.3 读取数据
private loadTodos(): void {
if (this.savedTodos && this.savedTodos !== '[]') {
this.todos = JSON.parse(this.savedTodos)
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
}
}
代码详解:
第2-3行:检查是否有保存的数据
if (this.savedTodos && this.savedTodos !== '[]') {
this.todos = JSON.parse(this.savedTodos)
-
if (this.savedTodos && this.savedTodos !== '[]'):- 检查
this.savedTodos是否有值 - 并且不是空数组的JSON字符串(
'[]') - 如果满足条件,说明有保存的数据,需要读取
- 检查
-
JSON.parse(this.savedTodos):- 将JSON字符串反序列化成数组
- 示例:
this.savedTodos = '[{"id":1,"text":"学习HarmonyOS","done":false}]' JSON.parse(this.savedTodos) // 返回: // [ // { id: 1, text: '学习HarmonyOS', done: false } // ]
-
this.todos = JSON.parse(this.savedTodos):- 将反序列化得到的数组赋值给
this.todos - 触发UI更新
- 将反序列化得到的数组赋值给
第4-10行:恢复 nextId
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
}
-
为什么要恢复
nextId?- 因为
nextId是用来生成新的待办事项的id的 - 如果每次启动应用都将
nextId重置为1,那么新添加的待办事项的id可能会与已有的待办事项的id冲突
- 因为
-
如何恢复
nextId?- 遍历
this.todos数组,找到最大的id - 将
nextId设置为maxId + 1
- 遍历
-
示例:
已有的待办事项: { id: 1, text: '学习HarmonyOS', done: false } { id: 2, text: '写博客', done: true } { id: 5, text: '跑步', done: false } 最大的 id 是 5 所以 nextId 应该设置为 5 + 1 = 6
3.5.4 何时读取数据?
我在 aboutToAppear() 生命周期方法中读取数据:
aboutToAppear(): void {
this.loadTodos()
}
-
aboutToAppear()是什么?- 这是ArkUI组件的生命周期方法
- 在组件即将出现时调用
- 适合做初始化操作(比如读取数据、启动定时器等)
-
为什么不在
aboutToAppear()中读取数据?- 其实也可以在
aboutToAppear()中读取数据 - 但是我选择在
aboutToAppear()中读取,因为这样更符合语义(组件即将出现时,准备好数据)
- 其实也可以在
第四部分:UI布局与样式设计(附完整代码与详解)
4.1 整体布局结构
我的待办事项应用的UI布局分为4个部分:
┌─────────────────────────────────────┐
│ 顶部标题栏 │ 1. 标题栏
├─────────────────────────────────────┤
│ 输入框 + 添加按钮 │ 2. 输入区域
├─────────────────────────────────────┤
│ 待办事项列表 │ 3. 列表区域
│ - 待办事项1 │
│ - 待办事项2 │
│ - ... │
├─────────────────────────────────────┤
│ 底部统计栏 │ 4. 统计栏
└─────────────────────────────────────┘
对应的UI代码结构:
build() {
Column() { // 纵向布局(整体)
Row() { ... } // 1. 标题栏
Row() { ... } // 2. 输入区域
List() { ... } // 3. 列表区域
Row() { ... } // 4. 统计栏
}
.width('100%')
.height('100%')
.backgroundColor('#000000')
}
4.2 标题栏
4.2.1 功能描述
标题栏显示应用的标题,以及已完成/总数的统计。
UI设计:
- 左侧:应用标题(“✅ 今日待办”)
- 右侧:统计信息(“已完成数 / 总数”)
4.2.2 代码实现
Row() {
Text('✅ 今日待办')
.fontSize(26).fontWeight(FontWeight.Bold).fontColor(Color.White)
Blank()
Text(`${this.completedCount} / ${this.totalCount}`)
.fontSize(18).fontColor('#8E8E93')
}
.width('100%').padding(20)
4.2.3 代码详解
Row() 布局:
- 横向布局容器
- 子组件从左到右排列
左侧标题:
Text('✅ 今日待办')
.fontSize(26).fontWeight(FontWeight.Bold).fontColor(Color.White)
-
Text('✅ 今日待办'):- 显示文本 “✅ 今日待办”
- ✅ 是Emoji,增加视觉吸引力
-
.fontSize(26):- 字体大小26像素
-
.fontWeight(FontWeight.Bold):- 字体加粗
-
.fontColor(Color.White):- 字体颜色白色
Blank() 组件:
Blank()
- 作用:占据剩余空间
- 效果:将后面的组件(统计信息)推到最右边
- 类似CSS的
margin-left: auto
右侧统计信息:
Text(`${this.completedCount} / ${this.totalCount}`)
.fontSize(18).fontColor('#8E8E93')
-
`${this.completedCount} / ${this.totalCount}`:- 模板字符串(类似JavaScript的模板字符串)
- 将
this.completedCount和this.totalCount插入到字符串中 - 示例:如果
completedCount是2,totalCount是5,显示"2 / 5"
-
.fontSize(18):- 字体大小18像素(比标题小一些)
-
.fontColor('#8E8E93'):- 字体颜色是浅灰色(#8E8E93)
- 这样不会抢标题的风头
容器样式:
.width('100%').padding(20)
-
.width('100%'):- 宽度填满父容器
-
.padding(20):- 四周内边距都是20像素
4.2.4 计算属性
completedCount 和 totalCount 是计算属性:
get completedCount(): number {
return this.todos.filter(item => item.done).length
}
get totalCount(): number {
return this.todos.length
}
completedCount:
get completedCount(): number {
return this.todos.filter(item => item.done).length
}
-
this.todos.filter(item => item.done):- 过滤出已完成的待办事项
- 返回一个新的数组
-
.length:- 返回数组的长度
- 即已完成的待办事项数量
totalCount:
get totalCount(): number {
return this.todos.length
}
- 返回
this.todos数组的长度 - 即总的待办事项数量
什么是计算属性?
- 计算属性是根据其他状态计算得到的属性
- 当依赖的状态变化时,计算属性会自动重新计算
- 在ArkUI中,用
get关键字定义计算属性
为什么不用普通方法?
- 其实用普通方法也可以:
private getCompletedCount(): number { return this.todos.filter(item => item.done).length } - 但是在UI中使用时,计算属性更简洁:
// 计算属性 Text(`${this.completedCount} / ${this.totalCount}`) // 普通方法 Text(`${this.getCompletedCount()} / ${this.getTotalCount()}`)
4.3 输入区域
4.3.1 功能描述
输入区域包含一个文本输入框和一个添加按钮。
UI设计:
- 左侧:文本输入框(占位符:“输入新待办…”)
- 右侧:添加按钮(“➕”)
4.3.2 代码实现
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 })
4.3.3 代码详解
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() })
-
TextInput({ placeholder: '输入新待办...', text: this.inputText }):- 创建一个文本输入框
placeholder:占位符(输入框为空时显示的提示文字)text:输入框的文本内容(双向绑定到this.inputText)
-
.layoutWeight(1):- 占据剩余空间
- 因为后面的添加按钮有固定宽度(48像素)
- 所以输入框会占据除按钮外的所有空间
-
.height(48):- 高度48像素
-
.backgroundColor('#2C2C2E'):- 背景色深灰色(#2C2C2E)
- 这是iOS风格的深色模式输入框颜色
-
.borderRadius(12):- 圆角半径12像素
- 让输入框看起来更圆润
-
.padding({ left: 16 }):- 左内边距16像素
- 让输入的文字不要贴着左边框
-
.fontColor(Color.White):- 输入的文字颜色是白色
-
.placeholderColor('#8E8E93'):- 占位符文字颜色是浅灰色(#8E8E93)
-
.onChange((value: string) => { this.inputText = value }):- 绑定文本变化事件
- 当输入框的内容变化时,将最新的内容保存到
this.inputText
-
.onSubmit(() => { this.addTodo() }):- 绑定提交事件(用户按回车键时触发)
- 调用
this.addTodo()方法,添加待办事项
添加按钮:
Button('➕')
.fontSize(22).width(48).height(48).borderRadius(24)
.backgroundColor('#FF9F0A').margin({ left: 8 })
.onClick(() => { this.addTodo() })
-
Button('➕'):- 创建一个按钮,文字是 “➕”(Emoji)
-
.fontSize(22):- 字体大小22像素
-
.width(48).height(48):- 宽度48像素,高度48像素
- 这是一个正方形按钮
-
.borderRadius(24):- 圆角半径24像素
- 因为宽度和高度都是48像素,所以这是一个正圆形按钮
-
.backgroundColor('#FF9F0A'):- 背景色是橙色(#FF9F0A)
- 这是iOS风格的操作按钮颜色
-
.margin({ left: 8 }):- 左边距8像素
- 让按钮和输入框之间有一点间距
-
.onClick(() => { this.addTodo() }):- 绑定点击事件
- 调用
this.addTodo()方法,添加待办事项
容器样式:
.width('100%').padding({ left: 20, right: 20 })
-
.width('100%'):- 宽度填满父容器
-
.padding({ left: 20, right: 20 }):- 左右内边距都是20像素
- 让输入区域不要贴着屏幕边缘
4.4 列表区域
4.4.1 功能描述
列表区域显示所有的待办事项,支持滚动。
UI设计:
- 使用
List组件显示待办事项列表 - 每个待办事项是一个
ListItem - 每个待办事项包含:
- 左侧:完成状态图标(⭕或✅)
- 中间:待办事项文本(已完成的有删除线)
- 右侧:删除按钮(🗑️)
4.4.2 代码实现
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())
}
.layoutWeight(1).width('100%')
.padding({ left: 20, right: 20 })
4.4.3 代码详解
List() 组件:
List({ space: 8 }) {
// ...
}
-
List():- 创建一个列表容器
- 类似Android的
RecyclerView、iOS的UITableView
-
{ space: 8 }:- 列表项之间的间距是8像素
ForEach() 循环:
ForEach(this.todos, (item: TodoItem) => {
ListItem() {
// ...
}
}, (item: TodoItem) => item.id.toString())
-
ForEach(this.todos, ...):- 遍历
this.todos数组 - 为每个待办事项创建一个
ListItem
- 遍历
-
(item: TodoItem) => { ... }:- 箭头函数,参数是
item(当前待办事项)
- 箭头函数,参数是
-
(item: TodoItem) => item.id.toString():- 为每个待办事项生成一个唯一的key
- 用
item.id转换成字符串作为key - 为什么要key?
- 提高列表渲染性能
- 当数组变化时,框架可以根据key判断哪些是新增的、哪些是删除的、哪些是需要移动的
ListItem() 组件:
ListItem() {
Row() {
// ...
}
.width('100%').padding(12)
.backgroundColor('#1C1C1E').borderRadius(12)
}
-
ListItem():- 创建一个列表项
- 每个
ListItem对应一个待办事项
-
容器样式:
.width('100%'):宽度填满父容器.padding(12):内边距12像素.backgroundColor('#1C1C1E'):背景色深灰色(#1C1C1E).borderRadius(12):圆角半径12像素
列表项的内部结构(Row() 布局):
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) })
}
完成状态图标(Button()):
Button(item.done ? '✅' : '⭕')
.fontSize(18).width(36).height(36)
.backgroundColor(Color.Transparent)
.onClick(() => { this.toggleTodo(item.id) })
-
Button(item.done ? '✅' : '⭕'):- 根据
item.done的值,决定显示哪个图标 - 如果
done是true,显示 ✅ - 如果
done是false,显示 ⭕
- 根据
-
.backgroundColor(Color.Transparent):- 背景色透明
- 因为我们用的是Emoji,不需要背景色
-
.onClick(() => { this.toggleTodo(item.id) }):- 绑定点击事件
- 点击时,调用
this.toggleTodo(item.id)方法,切换完成状态
待办事项文本(Text()):
Text(item.text)
.fontSize(18)
.fontColor(item.done ? '#555555' : Color.White)
.decoration({
type: item.done
? TextDecorationType.LineThrough
: TextDecorationType.None
})
.layoutWeight(1).padding({ left: 8 })
-
Text(item.text):- 显示待办事项的文本
-
.fontColor(item.done ? '#555555' : Color.White):- 根据
item.done的值,决定字体颜色 - 如果
done是true,显示深灰色(#555555) - 如果
done是false,显示白色
- 根据
-
.decoration({ ... }):- 设置文本装饰(如下划线、删除线等)
type: item.done ? TextDecorationType.LineThrough : TextDecorationType.None:- 如果
done是true,显示删除线 - 如果
done是false,不显示装饰
- 如果
-
.layoutWeight(1):- 占据剩余空间
- 这样删除按钮会靠右显示
-
.padding({ left: 8 }):- 左边距8像素
- 让文本和完成状态图标之间有一点间距
删除按钮(Button()):
Button('🗑️')
.fontSize(16).backgroundColor(Color.Transparent)
.onClick(() => { this.deleteTodo(item.id) })
-
Button('🗑️'):- 创建一个按钮,文字是 “🗑️”(Emoji)
-
.backgroundColor(Color.Transparent):- 背景色透明
- 因为我们用的是Emoji,不需要背景色
-
.onClick(() => { this.deleteTodo(item.id) }):- 绑定点击事件
- 点击时,调用
this.deleteTodo(item.id)方法,删除待办事项
列表容器样式:
.layoutWeight(1).width('100%')
.padding({ left: 20, right: 20 })
-
.layoutWeight(1):- 占据剩余空间
- 因为标题栏和输入区域都有固定高度
- 所以列表区域会占据除它们之外的所有空间
-
.width('100%'):- 宽度填满父容器
-
.padding({ left: 20, right: 20 }):- 左右内边距都是20像素
- 让列表不要贴着屏幕边缘
4.5 统计栏
4.5.1 功能描述
统计栏显示待办事项的总数,以及"清除已完成"按钮(仅当有已完成的待办事项时显示)。
UI设计:
- 左侧:总数统计(“共 X 项”)
- 右侧:"清除已完成"按钮(条件显示)
4.5.2 代码实现
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 })
4.5.3 代码详解
Row() 布局:
- 横向布局容器
- 子组件从左到右排列
左侧总数统计:
Text(`共 ${this.totalCount} 项`)
.fontSize(14).fontColor('#8E8E93')
-
`共 ${this.totalCount} 项`:- 模板字符串
- 显示 “共 X 项”,其中 X 是
this.totalCount的值
-
.fontSize(14):- 字体大小14像素(比较小,因为这是次要信息)
-
.fontColor('#8E8E93'):- 字体颜色浅灰色(#8E8E93)
Blank() 组件:
Blank()
- 作用:占据剩余空间
- 效果:将后面的"清除已完成"按钮推到最右边
右侧"清除已完成"按钮(条件显示):
if (this.hasCompleted) {
Text('清除已完成')
.fontSize(14).fontColor('#FF9F0A')
.onClick(() => { this.clearDone() })
}
-
if (this.hasCompleted):- 只有当有已完成的待办事项时,才显示"清除已完成"按钮
this.hasCompleted是一个计算属性(后面会详解)
-
Text('清除已完成'):- 显示文本 “清除已完成”
-
.fontSize(14):- 字体大小14像素
-
.fontColor('#FF9F0A'):- 字体颜色橙色(#FF9F0A)
- 橙色表示可点击(类似链接)
-
.onClick(() => { this.clearDone() }):- 绑定点击事件
- 点击时,调用
this.clearDone()方法,清除所有已完成的待办事项
容器样式:
.width('100%').padding(20)
.backgroundColor('#1C1C1E')
.borderRadius({ topLeft: 16, topRight: 16 })
-
.width('100%'):- 宽度填满父容器
-
.padding(20):- 内边距20像素
-
.backgroundColor('#1C1C1E'):- 背景色深灰色(#1C1C1E)
-
.borderRadius({ topLeft: 16, topRight: 16 }):- 只设置左上角和右上角的圆角
- 让统计栏看起来像是一个"卡片"的顶部
4.5.4 计算属性
hasCompleted 是一个计算属性:
get hasCompleted(): boolean {
return this.completedCount > 0
}
- 作用:判断是否有已完成的待办事项
- 返回值:
true(有)或false(没有) - 实现:如果
this.completedCount > 0,返回true,否则返回false
第五部分:踩坑记录与解决方案
坑1:@State 装饰的数组,直接修改不会触发UI更新
问题描述
我一开始是这样实现 addTodo() 方法的:
// 错误实现
private addTodo(): void {
const text = this.inputText.trim()
if (text === '') return
const newItem: TodoItem = {
id: this.nextId++,
text: text,
done: false
}
this.todos.push(newItem) // ❌ 错误!不会触发UI更新
this.inputText = ''
this.saveTodos()
}
问题:调用 this.todos.push(newItem) 后,UI没有更新(列表没有显示新添加的待办事项)。
原因分分析
在ArkUI中,@State 装饰的状态变量,只有在重新赋值时才会触发UI更新。
直接调用 this.todos.push(newItem),虽然修改了数组的内容,但是 this.todos 这个引用没有变,所以ArkUI认为状态没有变化,不会触发UI更新。
解决方案
必须重新赋值给 this.todos:
// 正确实现
private addTodo(): void {
const text = this.inputText.trim()
if (text === '') return
const newItem: TodoItem = {
id: this.nextId++,
text: text,
done: false
}
this.todos = [newItem].concat(this.todos) // ✅ 正确!会触发UI更新
this.inputText = ''
this.saveTodos()
}
类似的坑:
| 错误用法 | 正确用法 |
|---|---|
this.todos.push(item) |
this.todos = this.todos.concat([item]) |
this.todos.unshift(item) |
this.todos = [item].concat(this.todos) |
this.todos.splice(index, 1) |
创建新数组,过滤掉要删除的元素 |
this.todos.sort() |
this.todos = this.todos.slice().sort() |
this.todos.reverse() |
this.todos = this.todos.slice().reverse() |
总结:
对于
@State装饰的数组和对象,必须通过重新赋值来触发UI更新,不能直接调用修改原数组/对象的方法。
坑2:@StorageLink 装饰的变量,类型必须是 string 或 number 或 boolean
问题描述
我一开始是这样定义 savedTodos 的:
// 错误定义
@StorageLink('todos_data') savedTodos: TodoItem[] = []
问题:编译报错,提示类型不匹配。
原因分分析
@StorageLink 装饰的变量,类型必须是 string 或 number 或 boolean。
不能用复杂类型(如数组、对象),因为 @StorageLink 需要将数据存储到本地存储,而本地存储只支持基本类型。
解决方案
将 savedTodos 定义为 string 类型,存储 todos 的JSON字符串:
// 正确定义
@StorageLink('todos_data') savedTodos: string = '[]'
然后,在保存和读取时,手动进行序列化和反序列化:
private saveTodos(): void {
this.savedTodos = JSON.stringify(this.todos) // 序列化成JSON字符串
}
private loadTodos(): void {
if (this.savedTodos && this.savedTodos !== '[]') {
this.todos = JSON.parse(this.savedTodos) // 反序列化成数组
// ...
}
}
坑3:ForEach 必须有唯一的key
问题描述
我一开始是这样写 ForEach 的:
// 错误写法
ForEach(this.todos, (item: TodoItem) => {
ListItem() {
// ...
}
})
问题:运行时报错,提示缺少key。
原因分分析
ForEach 必须有一个唯一的key,用来标识每个列表项。
这个key的作用:
- 提高渲染性能:当数组变化时,框架可以根据key判断哪些是新增的、哪些是删除的、哪些是需要移动的
- 保持组件状态:如果某个列表项有内部状态(比如输入框的文字),key可以用来保持这个状态
解决方案
为 ForEach 提供唯一的key:
// 正确写法
ForEach(this.todos, (item: TodoItem) => {
ListItem() {
// ...
}
}, (item: TodoItem) => item.id.toString()) // ✅ 提供唯一的key
key的要求:
- 唯一:每个列表项的key必须唯一
- 稳定:key不能变化(否则会失去性能优化效果)
为什么用 item.id.toString() 作为key?
- 唯一:因为
item.id是唯一的 - 稳定:因为
item.id不会变化(一旦分配就不会修改)
不要用数组索引作为key!
// ❌ 错误!不要用数组索引作为key
ForEach(this.todos, (item: TodoItem, index: number) => {
ListItem() {
// ...
}
}, (item: TodoItem, index: number) => index.toString())
原因:如果数组的顺序发生变化(比如删除、插入元素),数组索引会变化,这样就失去了key的作用。
坑4:输入框的 onChange 事件会触发多次
问题描述
我一开始是这样绑定输入框的 onChange 事件的:
TextInput({ placeholder: '输入新待办...', text: this.inputText })
.onChange((value: string) => {
console.info('onChange: ' + value) // 打印日志
this.inputText = value
})
问题:每次输入一个字符,都会触发一次 onChange 事件,导致日志打印很多次。
原因分分析
这是正常行为。onChange 事件会在输入框的内容每次变化时触发。
如果你输入 “abc”,会触发3次 onChange 事件:
- 输入 “a” 时触发
- 输入 “b” 时触发
- 输入 “c” 时触发
解决方案
这是正常行为,不需要解决。但是如果你想要防抖(延迟处理),可以用以下方法:
private searchTimer: number = 0
TextInput({ placeholder: '输入新待办...', text: this.inputText })
.onChange((value: string) => {
if (this.searchTimer) {
clearTimeout(this.searchTimer)
}
this.searchTimer = setTimeout(() => {
this.inputText = value
}, 300) // 延迟300ms
})
但是,对于待办事项应用,不需要防抖,因为:
onChange事件只是更新this.inputText,开销很小- 防抖会让用户输入时有"延迟感",影响体验
第六部分:完整代码清单
以下是待办事项应用的完整代码(复制粘贴到 Index.ets 中即可运行):
interface TodoItem {
id: number
text: string
done: boolean
}
@Entry
@Component
struct Index {
@State todos: TodoItem[] = []
@State inputText: string = ''
@StorageLink('todos_data') savedTodos: string = '[]'
private nextId: number = 1
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
}
private addTodo(): void {
const text = this.inputText.trim()
if (text === '') return
const newItem: TodoItem = {
id: this.nextId++,
text: text,
done: false
}
this.todos = [newItem].concat(this.todos)
this.inputText = ''
this.saveTodos()
}
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) {
newTodos.push({ id: item.id, text: item.text, done: !item.done })
} else {
newTodos.push(item)
}
}
this.todos = newTodos
this.saveTodos()
}
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
this.saveTodos()
}
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
this.saveTodos()
}
private saveTodos(): void {
this.savedTodos = JSON.stringify(this.todos)
}
private loadTodos(): void {
if (this.savedTodos && this.savedTodos !== '[]') {
this.todos = JSON.parse(this.savedTodos)
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()
}
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())
}
.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')
}
}
第七部分:运行截图

第八部分:性能优化与未来改进
8.1 性能优化
8.1.1 使用 trackBy 提高列表渲染性能
在 ForEach 中,我已经使用了唯一的key:
ForEach(this.todos, (item: TodoItem) => {
// ...
}, (item: TodoItem) => item.id.toString())
这已经可以提高列表渲染性能了。
8.1.2 避免不必要的重新渲染
在我的代码中,当 todos 变化时,只有列表区域会重新渲染,其他区域(标题栏、输入区域、统计栏)不会重新渲染。
这是因为ArkUI的细粒度响应式更新机制:只有依赖了变化状态的部分,才会重新渲染。
8.2 未来改进
8.2.1 功能改进
| 功能 | 优先级 | 难度 | 说明 |
|---|---|---|---|
| 待办事项分类 | 高 | ⭐⭐ | 增加"工作"、“生活”、"学习"等分类 |
| 设置提醒时间 | 中 | ⭐⭐⭐⭐ | 到时间后推送通知 |
| 云同步 | 低 | ⭐⭐⭐⭐⭐ | 需要后端服务器和华为账号 |
| 语音输入 | 低 | ⭐⭐⭐ | 调用华为语音识别API |
8.2.2 UI改进
| 改进点 | 优先级 | 难度 | 说明 |
|---|---|---|---|
| 深色/浅色模式切换 | 高 | ⭐ | 提供两种主题 |
| 动画效果 | 中 | ⭐⭐⭐ | 添加/删除/完成时的动画 |
| 拖拽排序 | 低 | ⭐⭐⭐⭐ | 支持拖拽排序待办事项 |
第九部分:总结与心得
9.1 项目总结
通过开发这个待办事项应用,我学到了:
-
HarmonyOS ArkUI开发的基础流程
- 创建项目、定义数据结构、实现功能、设计UI、调试运行
-
ArkUI的状态管理机制
@State、@StorageLink等装饰器的用法- 响应式编程的思想
-
数据持久化方案
- 使用
@StorageLink实现自动持久化
- 使用
-
列表渲染性能优化
- 为
ForEach提供唯一的key
- 为
-
iOS风格UI设计
- 深色模式、圆角、间距、字体颜色等
9.2 开发心得
心得1:一定要理解状态管理机制
ArkUI的状态管理机制是最核心的概念。如果你不理解 @State、@Prop、@Link、@StorageLink 等装饰器的作用,就会写出很多bug。
建议:
- 仔细阅读官方文档的"状态管理"章节
- 多写demo,亲自体验不同装饰器的效果
心得2:多用计算属性
计算属性可以让代码更简洁、更易维护。
示例:
// ❌ 不用计算属性(代码冗长)
Text(`${this.todos.filter(item => item.done).length} / ${this.todos.length}`)
// ✅ 用计算属性(代码简洁)
Text(`${this.completedCount} / ${this.totalCount}`)
心得3:一定要注意性能
虽然这个待办事项应用很简单,但是也要注意性能。
建议:
- 为
ForEach提供唯一的key - 避免在
build()方法中做复杂计算 - 如果列表很长,考虑分页加载或虚拟列表
相关资源
- 华为开发者官网:https://developer.harmonyos.com
- ArkUI官方文档:https://developer.harmonyos.com/cn/docs/documentation/doc-guides/arkui
更多推荐


所有评论(0)