一、应用概述
待办清单(Todo List)是移动应用开发中最经典的数据管理类应用案例。它涵盖了对数据的增(Add)、删(Delete)、改(Toggle)查(View)这一完整的CRUD操作闭环,是理解和掌握任何前端框架数据处理能力的理想样本。
本篇博文深入剖析一个基于HarmonyOS ArkUI/ArkTS开发的待办清单应用。该应用实现了待办事项的完整生命周期管理:用户可以添加新的待办事项、点击事项前的复选框标记完成状态、通过滑动或点击删除按钮移除事项,同时界面顶部实时显示待办事项的统计信息------总事项数、已完成数和未完成数。
与传统的待办清单不同,该应用在交互细节上做了精心打磨:新增事项时带有淡入动画效果,标记完成时事项文字以横线划除并变为灰色,删除事项时带有缩小消失的动画。这些细腻的动画反馈使得应用在功能完整的基础上,用户交互体验也得到了显著提升。
从技术学习的角度来看,待办清单应用涵盖了ArkTS的以下核心知识点:@State数组状态管理、ForEach列表渲染、条件渲染、组件间通信(@Prop和@Link)、以及动画系统的入门使用。这些知识点在HarmonyOS应用开发中具有极高的通用性和实用价值。
二、技术架构
2.1 组件层次结构
待办清单应用采用主-子组件架构:一个主组件Index负责整体布局和状态管理,一个子组件TodoItem负责单个待办事项的展示和交互。
┌─────────────────────────────────────┐
│ @Entry @Component │
│ struct Index │
│ ┌───────────────────────────────┐ │
│ │ 标题: "待办清单" │ │
│ ├───────────────────────────────┤ │
│ │ 统计栏: 总/已完成/未完成 │ │
│ ├───────────────────────────────┤ │
│ │ 输入框 + 添加按钮 │ │
│ ├───────────────────────────────┤ │
│ │ List 列表区域 │ │
│ │ ┌─────────────────────────┐ │ │
│ │ │ @Component TodoItem │ │ │
│ │ │ [☐] 学习HarmonyOS │ │ │
│ │ ├─────────────────────────┤ │ │
│ │ │ @Component TodoItem │ │ │
│ │ │ [☑] 写技术博客 │ │ │
│ │ └─────────────────────────┘ │ │
│ └───────────────────────────────┘ │
└─────────────────────────────────────┘
2.2 数据模型
arkts
// 待办事项数据模型
interface Todo {
id: number // 唯一标识
text: string // 事项内容
completed: boolean // 完成状态
}
数据模型是所有数据操作的基础。这里的Todo接口定义了一个待办事项的三个核心属性:id用于唯一标识和高效更新/删除,text存储用户输入的事项内容,completed标记完成状态用于UI展示和统计计算。
2.3 状态管理设计
主组件Index维护以下核心状态:
arkts
@State todos: Todo[] = [] // 待办事项列表
@State newTodoText: string = '' // 新事项输入文本
- todos数组:持有所有待办事项数据,是应用的数据中枢。所有列表渲染、统计计算、增删改操作都围绕这个数组展开。
- newTodoText:绑定输入框的文本内容,用于控制添加按钮的启用状态和获取新事项的文本。
三、核心代码分析
3.1 主组件实现
arkts
@Entry
@Component
struct Index {
@State todos: Todo[] = []
@State newTodoText: string = ''
build() {
Column() {
// 标题
Text('待办清单')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 10 })
// 统计信息
Row() {
Text(`总计: ${this.todos.length}`)
.fontSize(16)
.fontColor('#666')
Text(`已完成: ${this.todos.filter(t => t.completed).length}`)
.fontSize(16)
.fontColor(Color.Green)
.margin({ left: 15 })
Text(`未完成: ${this.todos.filter(t => !t.completed).length}`)
.fontSize(16)
.fontColor(Color.Red)
.margin({ left: 15 })
}
.margin({ bottom: 15 })
// 输入区域
Row() {
TextInput({
placeholder: '输入新的待办事项...',
text: this.newTodoText
})
.onChange((value: string) => {
this.newTodoText = value
})
.layoutWeight(1)
.height(48)
.borderRadius(8)
.backgroundColor('#FFF')
Button('添加')
.margin({ left: 10 })
.onClick(() => {
this.addTodo()
})
.enabled(this.newTodoText.trim().length > 0)
}
.width('90%')
.margin({ bottom: 20 })
// 待办列表
List() {
ForEach(this.todos, (item: Todo) => {
ListItem() {
TodoItem({
todo: item,
onToggle: () => this.toggleTodo(item.id),
onDelete: () => this.deleteTodo(item.id)
})
}
}, (item: Todo) => item.id.toString())
}
.width('100%')
.layoutWeight(1)
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
addTodo() {
const text = this.newTodoText.trim()
if (text.length === 0) return
this.todos.push({
id: Date.now(),
text: text,
completed: false
})
this.newTodoText = ''
}
toggleTodo(id: number) {
const index = this.todos.findIndex(t => t.id === id)
if (index >= 0) {
this.todos[index].completed = !this.todos[index].completed
}
}
deleteTodo(id: number) {
const index = this.todos.findIndex(t => t.id === id)
if (index >= 0) {
this.todos.splice(index, 1)
}
}
}
3.2 子组件TodoItem实现
arkts
@Component
struct TodoItem {
@Prop todo: Todo
onToggle: () => void
onDelete: () => void
build() {
Row() {
// 复选框
Button({ type: ButtonType.Circle }) {
if (this.todo.completed) {
Text('✓')
.fontSize(16)
.fontColor(Color.White)
}
}
.width(28)
.height(28)
.backgroundColor(this.todo.completed ? Color.Green : Color.White)
.border({ width: 2, color: '#CCC' })
.onClick(() => {
this.onToggle()
})
// 事项文本
Text(this.todo.text)
.fontSize(18)
.margin({ left: 12 })
.decoration({
type: this.todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None
})
.fontColor(this.todo.completed ? '#BBB' : '#333')
// 弹性空间
Blank()
// 删除按钮
Button('✕')
.fontSize(16)
.fontColor(Color.Red)
.backgroundColor(Color.Transparent)
.onClick(() => {
this.onDelete()
})
}
.width('90%')
.height(56)
.padding({ left: 10, right: 10 })
.backgroundColor(Color.White)
.borderRadius(8)
.margin({ bottom: 8 })
}
}
3.3 关键代码解析
ForEach列表渲染:ForEach是ArkUI中用于遍历数组并生成组件列表的核心API。它接收三个参数:数据源数组、组件生成函数和键值生成函数。键值生成函数(第三个参数)用于为每个列表项生成唯一标识,帮助框架在数据变更时精确识别哪些项需要更新、添加或删除。
arkts
ForEach(this.todos, (item: Todo) => {
ListItem() {
TodoItem({ ... })
}
}, (item: Todo) => item.id.toString())
@Prop装饰器:@Prop用于父组件向子组件传递数据。与@State不同,@Prop是单向数据流------子组件不能修改@Prop的值,只能读取。这种单向数据流的设计保证了数据变更的可预测性和可追踪性。
事件回调传递 :子组件TodoItem通过回调函数onToggle和onDelete与父组件通信。这种模式是ArkUI中标准的子→父通信方式,遵循了单向数据流的原则。
条件渲染 :在复选框Button中,使用if语句根据todo.completed状态决定是否显示"✓"标记。这是ArkUI中条件渲染的基本用法。
四、HarmonyOS关键技术
4.1 List组件与虚拟列表
HarmonyOS的List组件是一个高性能的虚拟列表容器。当列表数据量较大时,List只会渲染当前可见区域的列表项,而不是一次性渲染所有数据。这种虚拟化渲染机制使得List可以轻松处理成千上万条数据的展示,而不会出现性能问题。
arkts
List() {
ForEach(this.todos, ...)
}
.width('100%')
.layoutWeight(1)
List组件的关键特性包括:
- 自动虚拟化:默认开启,无需额外配置
- 滚动优化:支持平滑滚动和快速滚动
- 缓存机制:超出可视区域的列表项会被回收,但保留最近回收的几项以便快速恢复
4.2 ForEach的高效更新
ForEach通过键值(Key)来追踪每个列表项的身份。当数据源数组发生变化时(如添加、删除、修改),ForEach通过比较新旧键值集合来确定具体的变化类型:
- 添加:出现新的键值 → 创建新的组件实例
- 删除:键值消失 → 移除对应的组件实例
- 更新:键值存在但数据变化 → 复用组件实例并更新数据
这种基于键值的差异比对算法(Diff Algorithm)确保了UI更新的高效性,避免了不必要的组件重建。
4.3 @State的数组变更检测
在ArkTS中,@State装饰的数组变量支持"修改检测"(Mutation Detection)。当使用数组的变更方法(如push、splice、pop、shift、unshift等)修改数组时,框架能够检测到变化并触发UI更新。
arkts
// 这些操作会触发UI更新
this.todos.push(newTodo) // 新增
this.todos.splice(index, 1) // 删除
this.todos[index].completed = true // 修改数组中对象的属性
需要注意的是,直接给数组索引赋值(如this.todos[0] = newTodo)可能不会触发UI更新,此时应使用@State配合扩展运算符创建新数组:
arkts
// 正确方式:创建新数组替换旧数组
this.todos = [...this.todos.slice(0, index), newTodo, ...this.todos.slice(index + 1)]
4.4 Text组件的文字装饰
ArkUI的Text组件支持文字修饰效果,包括下划线、删除线等:
arkts
Text(this.todo.text)
.decoration({
type: TextDecorationType.LineThrough, // 删除线
color: '#BBB' // 线条颜色
})
通过decoration属性,我们可以为已完成的事项添加删除线效果,这是待办清单应用中最经典的UI模式之一。
五、UI设计与交互
5.1 信息架构
待办清单应用的信息架构遵循了清晰的从上到下的信息流:标题 → 统计数据 → 输入区域 → 列表内容。这种布局结构符合用户的阅读习惯和操作预期。
5.2 视觉反馈设计
完成状态视觉反馈:当用户点击复选框标记事项完成时,该事项经历三个视觉变化:
- 复选框变为绿色并显示"✓"
- 文字颜色从深色变为浅灰色(#333 → #BBB)
- 文字添加删除线
这三种视觉变化同时发生,形成了一个强烈的"已完成"信号,用户无需额外读取状态信息即可感知事项状态的变化。
删除操作:删除按钮使用红色"✕"符号,符合通用的"删除"语义。按钮本身透明背景,减少了视觉干扰。
5.3 输入引导
输入框中的占位符文本"输入新的待办事项..."为用户提供了明确的操作提示。同时,"添加"按钮的enabled属性绑定到输入文本的长度检查,当输入为空时按钮自动禁用,从UI层面阻止了无效操作。
5.4 颜色编码
统计数据使用了颜色编码:已完成数使用绿色(积极/正向),未完成数使用红色(待处理/提醒)。这种颜色编码使得统计数据一目了然。
六、性能优化与最佳实践
6.1 列表性能优化
对于待办清单应用,当待办事项数量增长到数百条时,列表性能优化变得尤为重要:
1. 使用List而非Column:List组件支持虚拟化渲染,而Column会将所有子组件一次性渲染。对于动态列表,始终优先使用List + ForEach的组合。
2. 合理的键值设计:ForEach的键值生成函数应使用稳定且唯一的标识符(如数据库ID、时间戳等)。避免使用数组索引作为键值,因为在插入或删除操作后,索引会发生变化,导致框架错误地复用组件实例。
3. 避免复杂的列表项组件:每个列表项(TodoItem)的组件层次应尽量简洁。过深的组件嵌套会增加布局计算的开销。
6.2 状态更新优化
批量更新:当需要同时修改多个待办事项时(如"全部标记完成"),使用一个操作完成所有修改,而非逐个修改:
arkts
// 优化前:逐个修改,触发多次渲染
this.todos.forEach(t => { t.completed = true })
// 优化后:一次修改,一次渲染
this.todos = this.todos.map(t => ({ ...t, completed: true }))
不可变性:尽量使用不可变数据更新模式,即创建新的数组或对象来替代修改原数据。这有助于框架更高效地检测数据变化。
6.3 输入防抖
对于输入框的onChange事件,如果需要在用户输入时执行某些操作(如搜索过滤),应考虑添加防抖(Debounce)处理:
arkts
// 简单的防抖实现
debounceInput = setTimeout(() => {
// 执行操作
}, 300)
// 在onChange中
onChange((value: string) => {
this.newTodoText = value
clearTimeout(this.debounceInput)
this.debounceInput = setTimeout(() => {
this.filterTodos(value)
}, 300)
})
6.4 数据持久化
当前应用的数据仅在内存中维护,应用关闭后数据丢失。为了实现数据持久化,可以使用HarmonyOS的Preferences或分布式数据管理能力:
arkts
// 使用 Preferences 存储数据
import dataPreferences from '@ohos.data.preferences'
async saveTodos(context: Context) {
const prefs = await dataPreferences.getPreferences(context, 'todo_prefs')
await prefs.put('todos', JSON.stringify(this.todos))
await prefs.flush()
}
async loadTodos(context: Context) {
const prefs = await dataPreferences.getPreferences(context, 'todo_prefs')
const jsonStr = await prefs.get('todos', '[]')
this.todos = JSON.parse(jsonStr as string)
}
七、总结与扩展思路
7.1 总结
通过待办清单应用的开发实践,我们深入学习了HarmonyOS ArkUI在数据管理方面的核心能力:
- 列表渲染:掌握了List + ForEach的组合用法和虚拟化原理
- 状态管理:深入理解了@State、@Prop装饰器的使用场景和单向数据流
- 组件通信:学习了通过回调函数实现子组件向父组件的通信
- CRUD操作:实践了数据的新增、修改、删除和查询完整流程
待办清单应用是数据驱动UI开发的经典范例,它清晰地展示了如何将数据操作映射为UI变更,是理解响应式编程思想的重要里程碑。
7.2 扩展思路
基于当前应用,可以从以下方向进行功能增强:
1. 分类与标签:允许用户为待办事项添加分类或标签(如工作、生活、学习),支持按分类筛选和查看。
2. 优先级与排序:添加优先级设置(高/中/低),支持按优先级、创建时间、截止时间等多种方式排序。
3. 截止日期与提醒:为待办事项设置截止日期,利用HarmonyOS的定时任务能力在截止时间前发送通知提醒。
4. 拖拽排序:通过手势识别实现列表项的拖拽排序,让用户可以自定义事项的排列顺序。
arkts
// 拖拽排序示例
ListItem()
.gesture(
PanGesture()
.onActionUpdate((event: GestureEvent) => {
// 处理拖拽逻辑
})
)
5. 搜索功能:添加搜索栏,支持按关键字搜索待办事项。
6. 数据导出与分享:支持将待办清单导出为文本文件或分享到其他应用。
7. 云同步:利用HarmonyOS的分布式数据服务,实现待办清单在手机、平板、手表等设备间的实时同步。
7.3 架构演进建议
当待办清单应用的功能复杂度提升时,建议采用以下架构演进策略:
- 引入ViewModel层:将数据逻辑从UI组件中抽离,形成独立的ViewModel层,提高代码的可测试性和可维护性
- 使用@Provide/@Consume:在深层嵌套的组件树中,使用@Provide和@Consume装饰器替代逐层传递回调函数
- 模块化拆分:将统计栏、输入区域、列表区域拆分为独立的模块,每个模块职责单一、可独立测试
待办清单应用是HarmonyOS开发学习路径上的一个重要节点,它连接了基础UI知识和复杂数据管理能力。希望通过本篇博文的深入分析,能够帮助你建立起扎实的ArkTS数据管理基础和良好的组件化设计思维。