HarmonyOS ArkUI实战:从零构建待办事项应用
本文介绍了一个基于HarmonyOS ArkUI框架开发的待办事项应用(TodoList)。该应用具备添加、标记完成、删除待办事项以及数据持久化等核心功能。项目采用ArkTS语言开发,使用DevEco Studio 4.0工具,支持HarmonyOS 4.0.0。文章详细说明了项目搭建、数据结构设计(包含id、text、done字段)、状态管理(@State和@StorageLink装饰器),并介
项目概述
本文所述的待办事项应用(TodoList)是一个基于HarmonyOS ArkUI框架开发的移动应用,主要功能包括:
- 添加待办事项
- 标记待办事项为已完成
- 删除待办事项
- 清除所有已完成的待办事项
- 数据持久化(应用关闭后数据不丢失)
项目信息:
| 项目信息 | 内容 |
|---|---|
| 项目名称 | 待办事项应用(TodoList) |
| 开发工具 | DevEco Studio 6.0及以上 |
| 开发语言 | ArkTS(TypeScript的HarmonyOS定制版) |
| UI框架 | ArkUI(声明式开发范式) |
| API版本 | 23 |
| 包名 | com.example.todolist |
| 项目路径 | E:\HMproject\Project\TodoList |
第一部分:项目搭建与基础结构设计
1.1 创建项目
- 打开DevEco Studio
- 点击 Create Project
- 选择 Application → Empty Ability
- 填写项目信息:
- Project Name: TodoList
- Bundle Name: com.example.todolist
- Save Location: E:\HMproject\Project\TodoList
- Compile SDK: 23
- Model: Stage模型
- 点击 Finish,等待项目创建完成
1.2 项目结构说明
项目创建完成后,主要目录结构如下:
TodoList/
├── entry/
│ └── src/main/
│ ├── ets/
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets # 应用入口
│ │ └── pages/
│ │ └── Index.ets # 主页面
│ ├── resources/
│ │ ├── base/
│ │ │ ├── element/
│ │ │ └── media/
│ │ └── rawfile/
│ └── module.json5 # 模块配置
├── AppScope/
│ └── app.json5 # 应用全局配置
└── build-profile.json5 # 构建配置
1.3 定义数据结构
待办事项的数据结构定义如下:
interface TodoItem {
id: number // 唯一标识符
text: string // 待办事项内容
done: boolean // 是否已完成
}
设计说明:
id:唯一标识符,用于区分不同的待办事项,避免直接操作数组索引带来的问题。text:待办事项的文本内容。done:布尔值,表示待办事项的完成状态。
1.4 设计状态管理
HarmonyOS ArkUI使用@State装饰器来管理组件状态:
@State todos: TodoItem[] = [] // 待办事项列表
@State inputText: string = '' // 输入框文本
对于需要持久化存储的数据,使用@StorageLink装饰器:
@StorageLink('todos_data') savedTodos: string = '[]'
@StorageLink会将数据与应用的首选项(Preferences)绑定,实现自动持久化。
第二部分:核心功能实现
2.1 添加待办事项
功能描述:用户在输入框中输入待办事项内容,点击添加按钮或按回车键,将新的待办事项添加到列表中。
实现代码:
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()
}
关键点:
- 使用
trim()方法去除首尾空格,避免添加空白待办事项。 - 使用
[newItem].concat(this.todos)而不是this.todos.push(newItem),因为@State装饰的数组直接修改不会触发UI更新,必须通过重新赋值来触发。 - 添加完成后清空输入框,并调用
saveTodos()保存数据。
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()
}
关键点:
- 通过
id找到目标待办事项,而不是通过数组索引。 - 创建新数组并重新赋值,以触发UI更新。
- 使用
!item.done翻转完成状态。
2.3 删除待办事项
功能描述:点击待办事项右边的删除按钮,从列表中移除该待办事项。
实现代码:
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()
}
关键点:
- 通过
id找到目标待办事项。 - 创建新数组,只推入
id不匹配的元素,相当于过滤掉目标待办事项。 - 重新赋值以触发UI更新。
2.4 清除所有已完成的待办事项
功能描述:点击底部的"清除已完成"按钮,一次性删除所有已完成的待办事项。
实现代码:
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()
}
关键点:
- 创建新数组,只推入未完成的待办事项。
- 重新赋值以触发UI更新。
2.5 数据持久化
保存数据:
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 // 恢复nextId,避免ID冲突
}
}
关键点:
- 使用
JSON.stringify()将数组序列化为字符串,然后赋值给savedTodos。 - 使用
JSON.parse()将字符串反序列化为数组。 - 恢复
nextId,避免ID冲突。
第三部分:UI布局与样式设计
3.1 整体布局结构
应用的UI布局分为四个部分:
- 标题栏:显示应用标题和统计信息(已完成数量/总数)。
- 输入区域:包含文本输入框和添加按钮。
- 列表区域:使用
List组件展示所有待办事项。 - 统计栏:显示总数和"清除已完成"按钮。
3.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)
设计说明:
- 使用
Row组件实现横向布局。 - 使用
Blank()组件占据剩余空间,将统计信息推到最右边。 - 标题使用白色大字体,统计信息使用灰色小字体。
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() })
Button('添加')
.fontSize(18).width(64).height(48).borderRadius(24)
.backgroundColor('#FF9F0A').margin({ left: 8 })
.onClick(() => { this.addTodo() })
}
.width('100%').padding({ left: 20, right: 20 })
设计说明:
- 文本输入框使用
layoutWeight(1)占据大部分空间。 - 添加按钮使用橙色背景,圆角设计。
- 绑定
onSubmit事件,用户按回车键时也可以添加待办事项。
3.4 列表区域
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 })
设计说明:
- 使用
ForEach组件遍历待办事项数组,生成列表项。 - 每个列表项包含:完成状态按钮、文本内容、删除按钮。
- 已完成的待办事项显示删除线和灰色文字。
- 必须提供唯一的
key(这里使用item.id.toString()),否则会出现渲染错误。
3.5 统计栏
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 })
设计说明:
- 只有当有已完成的待办事项时,才显示"清除已完成"按钮。
- 使用条件渲染(
if (this.hasCompleted))控制按钮的显示与隐藏。
第四部分:踩坑记录与解决方案
坑1:@State装饰的数组,直接修改不会触发UI更新
问题描述:
调用this.todos.push(newItem)后,数据已经添加,但UI没有更新。
解决方案:
对于@State装饰的数组和对象,必须通过重新赋值来触发UI更新:
// 错误做法
this.todos.push(newItem)
// 正确做法
this.todos = [newItem].concat(this.todos)
坑2:@StorageLink装饰的变量,类型必须是string/number/boolean
问题描述:
尝试直接存储对象数组,导致持久化失败。
解决方案:
将对象数组序列化为JSON字符串:
this.savedTodos = JSON.stringify(this.todos) // 存储
this.todos = JSON.parse(this.savedTodos) // 读取
坑3:ForEach必须有唯一的key
问题描述:
使用ForEach组件时,如果没有提供唯一的key,会导致列表渲染错误。
解决方案:
在ForEach的第二个参数中,提供一个返回唯一key的函数:
ForEach(this.todos, (item: TodoItem) => {
// 列表项内容
}, (item: TodoItem) => item.id.toString()) // 唯一的key
坑4:输入框的onChange事件会触发多次
问题描述:
输入框的onChange事件会在每次文本变化时触发,可能导致性能问题。
解决方案:
使用防抖(debounce)或节流(throttle)技术,或直接使用onSubmit事件(用户按回车键时触发)。
第五部分:完整代码清单
// 定义待办事项的数据结构
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()
}
// 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(18).width(64).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')
}
}
第六部分:运行截图

第七部分:总结
本文详细介绍了如何使用HarmonyOS ArkUI框架从零开始构建一个功能完整的待办事项应用。主要内容包括:
- 项目搭建:创建HarmonyOS项目,了解项目结构。
- 数据结构设计:定义待办事项的数据结构,设计状态管理方案。
- 核心功能实现:添加、标记完成、删除、清除已完成、数据持久化。
- UI布局与样式设计:标题栏、输入区域、列表区域、统计栏。
- 踩坑记录与解决方案:总结开发过程中遇到的问题及解决方案。
- 完整代码清单:提供完整可运行的代码。
通过本文的学习,读者可以掌握HarmonyOS ArkUI框架的基本使用方法,并能够独立开发简单的移动应用。
相关资源:
- 华为开发者官网:https://developer.harmonyos.com
- ArkUI官方文档:https://developer.harmonyos.com/cn/docs/documentation/doc-guides/arkui
更多推荐



所有评论(0)