待办事项应用示例
这是一个完整的 HarmonyOS 待办事项应用,演示了状态管理、列表渲染、数据持久化等核心功能。
功能特性
- 添加待办事项
- 标记完成/未完成
- 删除事项
- 数据持久化(Preferences)
- 列表动画
项目结构
todo-app/
├── entry/src/main/ets/
│ ├── entryability/
│ │ └── EntryAbility.ets
│ ├── pages/
│ │ └── Index.ets # 主页面
│ ├── components/
│ │ ├── TodoItem.ets # 待办项组件
│ │ └── AddTodo.ets # 添加待办组件
│ └── model/
│ └── TodoModel.ets # 数据模型
└── resources/核心代码
1. 数据模型
typescript
// model/TodoModel.ets
import { preferences } from '@kit.ArkData'
@Observed
export class Todo {
id: number
title: string
completed: boolean
createdAt: number
constructor(title: string) {
this.id = Date.now()
this.title = title
this.completed = false
this.createdAt = Date.now()
}
}
export class TodoModel {
private static pref: preferences.Preferences | null = null
private static readonly KEY = 'todo_list'
static async init(): Promise<void> {
const context = getContext()
TodoModel.pref = await preferences.getPreferences(context, 'todo_app')
}
static async loadTodos(): Promise<Todo[]> {
if (!TodoModel.pref) return []
const data = await TodoModel.pref.get(TodoModel.KEY, '[]') as string
const arr = JSON.parse(data)
return arr.map((item: object) => Object.assign(new Todo(''), item))
}
static async saveTodos(todos: Todo[]): Promise<void> {
if (!TodoModel.pref) return
await TodoModel.pref.put(TodoModel.KEY, JSON.stringify(todos))
await TodoModel.pref.flush()
}
}2. 待办项组件
typescript
// components/TodoItem.ets
import { Todo } from '../model/TodoModel'
@Component
export struct TodoItemView {
@ObjectLink todo: Todo
onToggle: () => void = () => {}
onDelete: () => void = () => {}
build() {
Row() {
Row({ space: 12 }) {
// 复选框
Stack() {
Circle()
.width(24)
.height(24)
.stroke(this.todo.completed ? '#4CAF50' : '#CCCCCC')
.strokeWidth(2)
.fill(this.todo.completed ? '#4CAF50' : Color.Transparent)
if (this.todo.completed) {
Text('✓')
.fontSize(14)
.fontColor(Color.White)
}
}
.width(24)
.height(24)
// 标题
Text(this.todo.title)
.fontSize(16)
.fontColor(this.todo.completed ? '#999999' : '#333333')
.decoration({
type: this.todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None
})
.layoutWeight(1)
}
.layoutWeight(1)
// 删除按钮
Button('删除')
.fontSize(12)
.fontColor('#FF4444')
.backgroundColor(Color.Transparent)
.onClick(() => this.onDelete())
}
.width('100%')
.padding(16)
.backgroundColor(Color.White)
.borderRadius(8)
.onClick(() => this.onToggle())
}
}3. 添加待办组件
typescript
// components/AddTodo.ets
@Component
export struct AddTodo {
@State inputText: string = ''
onAdd: (text: string) => void = () => {}
build() {
Row({ space: 12 }) {
TextInput({ placeholder: '输入待办事项...', text: this.inputText })
.placeholderColor('#999999')
.height(48)
.layoutWeight(1)
.onChange((value) => {
this.inputText = value
})
Button('添加')
.width(80)
.height(48)
.backgroundColor('#007DFF')
.enabled(this.inputText.length > 0)
.onClick(() => {
this.onAdd(this.inputText)
this.inputText = ''
})
}
.width('100%')
.padding(16)
}
}4. 主页面
typescript
// pages/Index.ets
import { Todo, TodoModel } from '../model/TodoModel'
import { TodoItemView } from '../components/TodoItem'
import { AddTodo } from '../components/AddTodo'
@Entry
@Component
struct TodoPage {
@State todos: Todo[] = []
@State isLoading: boolean = true
aboutToAppear() {
this.loadTodos()
}
async loadTodos() {
await TodoModel.init()
this.todos = await TodoModel.loadTodos()
this.isLoading = false
}
async saveTodos() {
await TodoModel.saveTodos(this.todos)
}
addTodo(title: string) {
const todo = new Todo(title)
this.todos.unshift(todo)
this.saveTodos()
}
toggleTodo(index: number) {
this.todos[index].completed = !this.todos[index].completed
this.saveTodos()
}
deleteTodo(index: number) {
this.todos.splice(index, 1)
this.saveTodos()
}
get completedCount(): number {
return this.todos.filter(t => t.completed).length
}
build() {
Column({ space: 16 }) {
// 标题栏
Row() {
Text('待办事项')
.fontSize(24)
.fontWeight(FontWeight.Bold)
Text(`${this.completedCount}/${this.todos.length}`)
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding(16)
// 添加区域
AddTodo({ onAdd: (text) => this.addTodo(text) })
// 列表
if (this.isLoading) {
LoadingProgress()
.width(40)
.height(40)
Text('加载中...')
.fontSize(14)
.fontColor('#999999')
} else if (this.todos.length === 0) {
Column({ space: 12 }) {
Text('📝')
.fontSize(48)
Text('暂无待办事项')
.fontSize(16)
.fontColor('#999999')
Text('点击上方添加按钮创建')
.fontSize(14)
.fontColor('#CCCCCC')
}
.layoutWeight(1)
.justifyContent(FlexAlign.Center)
} else {
List({ space: 8 }) {
ForEach(this.todos, (todo: Todo, index: number) => {
ListItem() {
TodoItemView({
todo: todo,
onToggle: () => this.toggleTodo(index),
onDelete: () => this.deleteTodo(index)
})
}
.transition({ type: TransitionType.All, opacity: 0 })
}, (todo: Todo) => todo.id.toString())
}
.width('100%')
.layoutWeight(1)
.padding({ left: 16, right: 16 })
.edgeEffect(EdgeEffect.Spring)
}
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
}
}运行效果
- 空状态显示提示信息
- 添加事项后显示在列表顶部
- 点击事项标记完成/未完成
- 左滑或点击删除按钮删除
- 数据自动保存,重启应用后恢复
学习要点
- @Observed + @ObjectLink:实现列表项的深度响应
- Preferences:轻量级数据持久化
- List + ForEach:高效列表渲染
- 组件拆分:AddTodo、TodoItemView 复用
- 状态提升:数据管理在页面层,通过回调传递操作