# 待办清单应用 — HarmonyOS状态管理与列表渲染实战

一、应用概述

待办清单(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通过回调函数onToggleonDelete与父组件通信。这种模式是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通过比较新旧键值集合来确定具体的变化类型:

  1. 添加:出现新的键值 → 创建新的组件实例
  2. 删除:键值消失 → 移除对应的组件实例
  3. 更新:键值存在但数据变化 → 复用组件实例并更新数据

这种基于键值的差异比对算法(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 视觉反馈设计

完成状态视觉反馈:当用户点击复选框标记事项完成时,该事项经历三个视觉变化:

  1. 复选框变为绿色并显示"✓"
  2. 文字颜色从深色变为浅灰色(#333 → #BBB)
  3. 文字添加删除线

这三种视觉变化同时发生,形成了一个强烈的"已完成"信号,用户无需额外读取状态信息即可感知事项状态的变化。

删除操作:删除按钮使用红色"✕"符号,符合通用的"删除"语义。按钮本身透明背景,减少了视觉干扰。

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在数据管理方面的核心能力:

  1. 列表渲染:掌握了List + ForEach的组合用法和虚拟化原理
  2. 状态管理:深入理解了@State、@Prop装饰器的使用场景和单向数据流
  3. 组件通信:学习了通过回调函数实现子组件向父组件的通信
  4. CRUD操作:实践了数据的新增、修改、删除和查询完整流程

待办清单应用是数据驱动UI开发的经典范例,它清晰地展示了如何将数据操作映射为UI变更,是理解响应式编程思想的重要里程碑。

7.2 扩展思路

基于当前应用,可以从以下方向进行功能增强:

1. 分类与标签:允许用户为待办事项添加分类或标签(如工作、生活、学习),支持按分类筛选和查看。

2. 优先级与排序:添加优先级设置(高/中/低),支持按优先级、创建时间、截止时间等多种方式排序。

3. 截止日期与提醒:为待办事项设置截止日期,利用HarmonyOS的定时任务能力在截止时间前发送通知提醒。

4. 拖拽排序:通过手势识别实现列表项的拖拽排序,让用户可以自定义事项的排列顺序。

arkts 复制代码
// 拖拽排序示例
ListItem()
  .gesture(
    PanGesture()
      .onActionUpdate((event: GestureEvent) => {
        // 处理拖拽逻辑
      })
  )

5. 搜索功能:添加搜索栏,支持按关键字搜索待办事项。

6. 数据导出与分享:支持将待办清单导出为文本文件或分享到其他应用。

7. 云同步:利用HarmonyOS的分布式数据服务,实现待办清单在手机、平板、手表等设备间的实时同步。

7.3 架构演进建议

当待办清单应用的功能复杂度提升时,建议采用以下架构演进策略:

  1. 引入ViewModel层:将数据逻辑从UI组件中抽离,形成独立的ViewModel层,提高代码的可测试性和可维护性
  2. 使用@Provide/@Consume:在深层嵌套的组件树中,使用@Provide和@Consume装饰器替代逐层传递回调函数
  3. 模块化拆分:将统计栏、输入区域、列表区域拆分为独立的模块,每个模块职责单一、可独立测试

待办清单应用是HarmonyOS开发学习路径上的一个重要节点,它连接了基础UI知识和复杂数据管理能力。希望通过本篇博文的深入分析,能够帮助你建立起扎实的ArkTS数据管理基础和良好的组件化设计思维。

相关推荐
爱写代码的阿木2 小时前
基于鸿蒙OS开发附近社交游戏平台(五)-构建配置与签名部署
游戏·华为·harmonyos
爱写代码的阿森3 小时前
鸿蒙三方库 | harmony-utils之CharUtil字符类型判断详解
华为·harmonyos·鸿蒙·huawei
Alter12303 小时前
从堆算力到算存网协同,华为给出了Agent时代的新解法
人工智能·华为
木木子223 小时前
# 计数器应用 — 从零开始的HarmonyOS ArkUI开发实战
华为·harmonyos
爱写代码的森4 小时前
鸿蒙三方库 | harmony-utils之PasteboardUtil剪贴板数据读写详解
服务器·华为·harmonyos·鸿蒙·huawei
SageMik4 小时前
通过 GitHub Actions 将 鸿蒙 HAR 包 发布到 OpenHarmony 三方库中心仓
harmonyos
爱写代码的阿森5 小时前
鸿蒙三方库 | harmony-utils之StrUtil字符串空值判断详解
华为·harmonyos·鸿蒙·huawei
jin1233225 小时前
HarmonyOS ArkTS API 24实现声明式 UI 框架与 @Builder 组件复用模式构建三标签页智能账单应用
ui·华为·harmonyos
DONSEE广东东信智能读卡器5 小时前
东信智能身份证阅读器、读卡器使用ArkTS 语言开发基于鸿蒙HarmonyOS 6.1版本的读取测试软件
harmonyos·鸿蒙·身份证阅读器·社保卡读卡器·东信est-100