项目概述

本文所述的待办事项应用(TodoList)是一个基于HarmonyOS ArkUI框架开发的移动应用,主要功能包括:

  • 添加待办事项
  • 标记待办事项为已完成
  • 删除待办事项
  • 清除所有已完成的待办事项
  • 数据持久化(应用关闭后数据不丢失)

项目信息

项目信息 内容
项目名称 待办事项应用(TodoList)
开发工具 DevEco Studio 6.0及以上
开发语言 ArkTS(TypeScript的HarmonyOS定制版)
UI框架 ArkUI(声明式开发范式)
API版本 23
包名 com.example.todolist
项目路径 E:\HMproject\Project\TodoList

第一部分:项目搭建与基础结构设计

1.1 创建项目

  1. 打开DevEco Studio
  2. 点击 Create Project
  3. 选择 ApplicationEmpty Ability
  4. 填写项目信息:
    • Project Name: TodoList
    • Bundle Name: com.example.todolist
    • Save Location: E:\HMproject\Project\TodoList
    • Compile SDK: 23
    • Model: Stage模型
  5. 点击 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()
}

关键点

  1. 使用trim()方法去除首尾空格,避免添加空白待办事项。
  2. 使用[newItem].concat(this.todos)而不是this.todos.push(newItem),因为@State装饰的数组直接修改不会触发UI更新,必须通过重新赋值来触发。
  3. 添加完成后清空输入框,并调用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()
}

关键点

  1. 通过id找到目标待办事项,而不是通过数组索引。
  2. 创建新数组并重新赋值,以触发UI更新。
  3. 使用!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()
}

关键点

  1. 通过id找到目标待办事项。
  2. 创建新数组,只推入id不匹配的元素,相当于过滤掉目标待办事项。
  3. 重新赋值以触发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()
}

关键点

  1. 创建新数组,只推入未完成的待办事项。
  2. 重新赋值以触发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冲突
  }
}

关键点

  1. 使用JSON.stringify()将数组序列化为字符串,然后赋值给savedTodos
  2. 使用JSON.parse()将字符串反序列化为数组。
  3. 恢复nextId,避免ID冲突。

第三部分:UI布局与样式设计

3.1 整体布局结构

应用的UI布局分为四个部分:

  1. 标题栏:显示应用标题和统计信息(已完成数量/总数)。
  2. 输入区域:包含文本输入框和添加按钮。
  3. 列表区域:使用List组件展示所有待办事项。
  4. 统计栏:显示总数和"清除已完成"按钮。

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框架从零开始构建一个功能完整的待办事项应用。主要内容包括:

  1. 项目搭建:创建HarmonyOS项目,了解项目结构。
  2. 数据结构设计:定义待办事项的数据结构,设计状态管理方案。
  3. 核心功能实现:添加、标记完成、删除、清除已完成、数据持久化。
  4. UI布局与样式设计:标题栏、输入区域、列表区域、统计栏。
  5. 踩坑记录与解决方案:总结开发过程中遇到的问题及解决方案。
  6. 完整代码清单:提供完整可运行的代码。

通过本文的学习,读者可以掌握HarmonyOS ArkUI框架的基本使用方法,并能够独立开发简单的移动应用。


相关资源

  • 华为开发者官网:https://developer.harmonyos.com
  • ArkUI官方文档:https://developer.harmonyos.com/cn/docs/documentation/doc-guides/arkui
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐