一、项目概述
1.1 项目背景

在 HarmonyOS NEXT 纯血鸿蒙开发体系中,数据驱动UI是核心开发思想,区别于传统命令式开发,声明式UI通过状态变更自动刷新页面,极大简化交互逻辑与页面渲染流程。

Todo待办事项项目作为全球开发者通用入门项目,麻雀虽小五脏俱全,浓缩了移动端开发的核心逻辑:结构化数据定义、用户交互监听、数据增删改查、视图动态渲染、状态实时同步,是新手从语法学习过渡到项目实战的最佳载体,为后续复杂应用开发夯实基础。

1.2 应用场景

本待办应用实用性极强,适配多场景日常使用:

  • 个人生活管理:记录购物清单、每日计划、运动、阅读等日常事项

  • 学习任务管理:整理课程作业、技术学习计划、考证备考任务

  • 职场工作管理:统计日常工作、跟进项目任务、梳理待办工作清单

  • 自律习惯养成:记录每日打卡任务,辅助养成良好作息与学习习惯

1.3 核心功能特性

摒弃残缺Demo功能,本项目实现全闭环商用级基础功能:

  1. 新增待办任务:输入任务内容,点击按钮提交,自动生成唯一ID与创建时间,拦截空任务提交

  2. 任务状态切换:复选框一键标记任务完成/进行中,已完成任务自动置灰+添加删除线

  3. 单任务删除:精准删除指定无用任务,数据实时同步,视图自动刷新

  4. 多维度筛选:支持全部任务、进行中任务、已完成任务三种模式无缝切换

  5. 实时数据统计:动态展示待完成数量、总任务数、完成进度

  6. 批量清理功能:一键清除所有已完成任务,简化列表管理

  7. 空状态适配:无任务、无对应筛选数据时,展示友好提示文案,提升用户体验

1.4 页面结构分区

项目采用模块化分层设计,页面层级清晰、解耦性高、便于迭代优化:

  • 头部统计区:展示应用标题、待完成任务数量、整体完成进度

  • 任务输入区:文本输入框+新增按钮,实现任务快速录入

  • 筛选切换区:标签切换三种任务筛选状态,交互直观

  • 任务列表区:动态渲染任务列表,支持弹性滚动、性能优化

  • 底部功能区:全局任务统计、一键清空已完成任务功能

1.5 技术栈详解

技术模块

详细说明

开发系统

HarmonyOS NEXT 纯血鸿蒙

API版本

API 23 最新稳定版

开发语言

ArkTS 强类型语法

UI框架

ArkUI 声明式UI

状态管理

@State 响应式状态管理

列表渲染

ForEach + List 高性能渲染

代码规范

@Builder 模块化组件拆分、单一职责设计

二、开发环境搭建与项目结构

2.1 项目创建步骤

基于 DevEco Studio 最新版创建纯净项目,适配 API23 规范,步骤如下:

  1. 打开 DevEco Studio,点击 Create HarmonyOS Project 创建新项目

  2. 选择官方纯净模板 Empty Ability,无冗余代码,适合新手开发

  3. 自定义项目名称为 TodoApp

  4. 编译SDK版本选择 API 23

  5. 等待项目依赖自动同步,完成初始化

2.2 标准项目目录结构

本项目遵循鸿蒙官方标准目录规范,层级清晰、易于维护拓展:

TodoApp/
├── AppScope/ # 应用全局配置目录
│ ├── app.json5 # 应用全局配置文件
│ └── resources/ # 全局静态资源
├── entry/ # 主功能模块
│ └── src/main/
│ ├── ets/ # ArkTS核心源码目录
│ │ ├── entryability/ # 应用入口Ability
│ │ └── pages/ # 页面组件目录(核心开发目录)
│ └── resources/ # 模块静态资源
├── build-profile.json5 # 项目构建配置
└── oh-package.json5 # 依赖管理配置

三、核心知识点深度精讲

本章深度拆解项目用到的10大核心知识点,从语法原理+实战用法+踩坑要点三维讲解,告别只会抄代码不懂原理的问题。

3.1 Interface 强类型数据接口

ArkTS基于TS强类型语法,Interface用于约束对象数据结构,统一数据格式,在编译阶段校验数据合法性,规避字段缺失、类型错误等隐性Bug,是规范化开发的基础。

本项目通过Interface定义单条待办任务的完整结构,统一所有任务数据格式:

interface TodoItem {
  id: number;        // 任务唯一标识,用于精准增删改查
  text: string;      // 任务文本内容
  completed: boolean;// 任务完成状态
  createdAt: string; // 任务创建时间
}

3.2 @State 数组响应式状态管理

@State是ArkUI最基础的响应式装饰器,不仅支持基础变量,完美适配数组类型状态。数组的增、删、改、筛选操作会自动触发UI刷新,实现数据驱动视图的核心效果。

核心状态定义,统一管理页面所有动态数据:

@State todos: TodoItem[] = [];      // 待办任务数组
@State newTodoText: string = '';    // 输入框绑定文本
@State nextId: number = 1;          // 任务自增唯一ID
@State filter: number = 0;          // 筛选标记:0-全部 1-进行中 2-已完成

核心踩坑点:直接修改数组索引可正常触发响应,搭配数组方法可实现完整的数据更新,无需手动刷新页面。

3.3 TextInput 文本输入组件

TextInput是核心交互组件,用于接收用户自定义输入,支持双向数据绑定、输入监听、提交事件等,本项目通过该组件实现任务内容录入。通过onChange实时同步输入内容到状态变量,实现输入内容实时联动。

3.4 Checkbox 复选框交互组件

Checkbox用于标记任务完成状态,通过select绑定任务布尔值状态,onChange监听状态切换,精准修改对应任务的completed属性,实现状态同步更新,搭配文本装饰实现视觉差异化。

3.5 List + ListItem 高性能列表

List是鸿蒙官方推荐的滚动列表组件,相比Column静态渲染,支持按需加载、缓存优化、弹性滚动,适合大量数据渲染,有效避免页面卡顿。ListItem作为最小渲染单元,承载单条任务UI布局。

3.6 ForEach 高性能循环渲染

ForEach是ArkUI专用循环渲染语法,用于遍历数组批量生成组件。通过唯一ID作为key,帮助框架精准识别变更项,实现局部刷新,大幅提升列表渲染性能,是列表开发的核心语法。

3.7 TextDecorationType 文本装饰

通过文本装饰属性,为已完成任务添加删除线,同时搭配字体置灰,直观区分任务状态,优化页面层级与用户视觉体验。

3.8 条件渲染 if/else

ArkUI支持原生if/else条件渲染,可根据任务数量、筛选状态动态切换页面内容,实现空状态适配、列表动态展示,让页面交互更人性化。

3.9 Blank 弹性填充组件

Blank组件可自动填充Row/Column剩余空白空间,快速实现两端对齐、居中自适应布局,简化传统布局嵌套冗余代码,让页面适配性更强。

3.10 @Builder 模块化组件封装

通过@Builder自定义构建函数,将页面拆分为多个独立功能模块,实现代码解耦、单一职责、复用性提升,彻底解决新手代码堆砌、逻辑混乱的问题。

四、完整可运行源码(零报错、直接部署)

路径:entry/src/main/ets/pages/Index.ets,全量替换即可运行,兼容模拟器与真机,适配API23所有特性。

// 定义待办任务结构化数据接口
interface TodoItem {
id: number;
text: string;
completed: boolean;
createdAt: string;
}

@Entry
@Component
struct Index {
  // 全局响应式状态管理
  @State todos: TodoItem[] = [];
  @State newTodoText: string = '';
  @State nextId: number = 1;
  @State filter: number = 0; // 0-全部 1-进行中 2-已完成

  build() {
    Column() {
      // 头部统计模块
      this.HeaderSection()
      // 任务输入模块
      this.InputSection()
      // 筛选标签模块
      this.FilterSection()
      // 任务列表模块
      this.TodoListSection()
      // 底部功能统计模块
      this.FooterSection()
    }
    .padding(16)
    .width('100%')
    .height('100%')
    .backgroundColor('#F8F9FA')
  }

  /**
   * 头部标题+统计区域
   */
  @Builder HeaderSection() {
    Row() {
      Column() {
        Text('待办事项')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .fontColor('#111827')

        Text(`${this.todos.filter(t => !t.completed).length} 项待完成`)
          .fontSize(12)
          .fontColor('#6B7280')
          .margin({ top: 4 })
      }
      .alignItems(HorizontalAlign.Start)

      Blank()

      // 完成进度徽章
      Text(`${this.todos.filter(t => t.completed).length}/${this.todos.length}`)
        .fontSize(14)
        .fontWeight(FontWeight.Medium)
        .fontColor('#6366F1')
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .backgroundColor('#E0E7FF')
        .borderRadius(999)
    }
    .width('100%')
    .padding({ bottom: 24 })
  }

  /**
   * 任务输入区域
   */
  @Builder InputSection() {
    Row() {
      TextInput({ placeholder: '添加新任务...', text: this.newTodoText })
        .layoutWeight(1)
        .height(52)
        .fontSize(16)
        .backgroundColor('#FFFFFF')
        .borderRadius(16)
        .onChange((value: string) => {
          this.newTodoText = value;
        })

      Button('+')
        .width(52)
        .height(52)
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .backgroundColor('#6366F1')
        .fontColor('#FFFFFF')
        .borderRadius(16)
        .margin({ left: 8 })
        .onClick(() => {
          this.addTodo();
        })
    }
    .width('100%')
    .margin({ bottom: 16 })
  }

  /**
   * 筛选标签切换区域
   */
  @Builder FilterSection() {
    Row() {
      Text('全部')
        .fontSize(14)
        .fontWeight(this.filter === 0 ? FontWeight.Medium : FontWeight.Regular)
        .fontColor(this.filter === 0 ? '#6366F1' : '#6B7280')
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .backgroundColor(this.filter === 0 ? '#E0E7FF' : '#FFFFFF')
        .borderRadius(8)
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .onClick(() => { this.filter = 0 })

      Text('进行中')
        .fontSize(14)
        .fontWeight(this.filter === 1 ? FontWeight.Medium : FontWeight.Regular)
        .fontColor(this.filter === 1 ? '#6366F1' : '#6B7280')
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .backgroundColor(this.filter === 1 ? '#E0E7FF' : '#FFFFFF')
        .borderRadius(8)
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .onClick(() => { this.filter = 1 })

      Text('已完成')
        .fontSize(14)
        .fontWeight(this.filter === 2 ? FontWeight.Medium : FontWeight.Regular)
        .fontColor(this.filter === 2 ? '#6366F1' : '#6B7280')
        .padding({ left: 16, right: 16, top: 8, bottom: 8 })
        .backgroundColor(this.filter === 2 ? '#E0E7FF' : '#FFFFFF')
        .borderRadius(8)
        .layoutWeight(1)
        .textAlign(TextAlign.Center)
        .onClick(() => { this.filter = 2 })
    }
    .width('100%')
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
    .padding(4)
    .margin({ bottom: 16 })
  }

  /**
   * 任务列表区域(含空状态适配)
   */
  @Builder TodoListSection() {
    if (this.getFilteredTodos().length === 0) {
      Column() {
        Text(this.filter === 0 ? '暂无任务' : this.filter === 1 ? '没有进行中的任务' : '没有已完成的任务')
          .fontSize(16)
          .fontColor('#9CA3AF')
          .margin({ bottom: 8 })

        if (this.filter === 0) {
          Text('点击上方输入框添加新任务')
            .fontSize(12)
            .fontColor('#9CA3AF')
        }
      }
      .layoutWeight(1)
      .justifyContent(FlexAlign.Center)
    } else {
      List() {
        ForEach(this.getFilteredTodos(), (todo: TodoItem) => {
          ListItem() {
            this.TodoItemComponent(todo)
          }
          .margin({ bottom: 8 })
        }, (todo: TodoItem) => todo.id.toString())
      }
      .layoutWeight(1)
      .width('100%')
      .cachedCount(10)
      .edgeEffect(EdgeEffect.Spring)
    }
  }

  /**
   * 单条任务Item组件
   */
  @Builder TodoItemComponent(todo: TodoItem) {
    Row() {
      Checkbox()
        .select(todo.completed)
        .selectedColor('#6366F1')
        .onChange((value: boolean) => {
          const index = this.todos.findIndex(t => t.id === todo.id);
          if (index >= 0) {
            this.todos[index].completed = value;
          }
        })

      Column() {
        Text(todo.text)
          .fontSize(16)
          .fontWeight(todo.completed ? FontWeight.Regular : FontWeight.Medium)
          .fontColor(todo.completed ? '#9CA3AF' : '#111827')
          .decoration({
            type: todo.completed ? TextDecorationType.LineThrough : TextDecorationType.None
          })

        Text(todo.createdAt)
          .fontSize(12)
          .fontColor('#9CA3AF')
          .margin({ top: 4 })
      }
      .layoutWeight(1)
      .margin({ left: 8 })
      .alignItems(HorizontalAlign.Start)

      Button('删除')
        .height(32)
        .fontSize(12)
        .backgroundColor('#FEE2E2')
        .fontColor('#EF4444')
        .borderRadius(8)
        .onClick(() => {
          this.todos = this.todos.filter(t => t.id !== todo.id);
        })
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(12)
  }

  /**
   * 底部统计与清空功能
   */
  @Builder FooterSection() {
    if (this.todos.length > 0) {
      Row() {
        Text(`${this.todos.length}`)
          .fontSize(12)
          .fontColor('#9CA3AF')

        Blank()

        Button('清除已完成')
          .fontSize(12)
          .height(32)
          .backgroundColor('#FEE2E2')
          .fontColor('#EF4444')
          .borderRadius(8)
          .onClick(() => {
            this.todos = this.todos.filter(t => !t.completed);
          })
      }
      .width('100%')
      .padding({ top: 16 })
    }
  }

  /**
   * 新增任务核心方法
   */
  addTodo(): void {
    // 去除首尾空格,拦截空任务提交
    if (this.newTodoText.trim()) {
      this.todos.push({
        id: this.nextId++,
        text: this.newTodoText.trim(),
        completed: false,
        createdAt: new Date().toLocaleDateString()
      });
      // 清空输入框,优化交互体验
      this.newTodoText = '';
    }
  }

  /**
   * 根据筛选条件过滤任务列表
   */
  getFilteredTodos(): TodoItem[] {
    switch (this.filter) {
      case 1:
        // 筛选进行中:未完成任务
        return this.todos.filter(t => !t.completed);
      case 2:
        // 筛选已完成任务
        return this.todos.filter(t => t.completed);
      default:
        // 默认展示全部任务
        return this.todos;
    }
  }
}

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

五、代码架构深度解析

5.1 模块化架构设计

本项目严格遵循单一职责、低耦合、高复用的企业级开发思想,通过@Builder将页面拆分为5大独立功能模块,每个模块仅负责自身UI渲染,核心业务逻辑单独封装工具方法,彻底避免代码堆砌、逻辑混乱的问题,便于后期迭代维护。

5.2 单向数据流机制

项目采用标准数据驱动流程:用户操作触发状态变更 → 状态实时更新 → 视图自动刷新,完全契合鸿蒙声明式UI核心思想,数据流转清晰、BUG率极低,逻辑闭环完整。

5.3 容错机制设计

内置多重容错逻辑,提升项目健壮性:拦截空任务提交、多场景空状态适配、通过唯一ID精准匹配修改/删除任务、筛选逻辑全覆盖,规避新手开发常见的逻辑漏洞。

5.4 性能优化细节

  • List组件开启cachedCount缓存,优化长列表滚动卡顿问题

  • ForEach使用唯一ID作为Key,实现列表局部刷新,减少无效渲染

  • 模块化组件拆分,按需渲染,提升页面加载效率

六、高频问题与解决方案

问题1:长列表滚动卡顿

问题原因:默认无列表缓存,大量列表项重复渲染,消耗性能

解决方案:为List组件添加cachedCount属性,预缓存指定数量列表项,大幅提升滚动流畅度

问题2:空任务可重复提交

问题原因:未对输入内容做非空校验,空格内容可正常提交

解决方案:通过trim()去除首尾空格,校验非空后再执行新增逻辑,拦截无效数据

问题3:筛选后数据不刷新

问题原因:筛选逻辑分散,未统一复用筛选方法

解决方案:封装全局统一筛选方法,所有列表数据均从该方法获取,保证数据一致性

问题4:应用重启数据丢失

问题原因:当前为内存级状态,无本地持久化存储

解决方案:可接入Preferences轻量存储,实现数据本地持久化,重启不丢失

七、项目进阶拓展方向

本项目架构可无缝迭代,适合进阶学习拓展,可新增以下功能:

  1. 数据持久化:接入Preferences/RDB数据库,实现任务数据永久保存

  2. 任务优先级:新增高/中/低优先级标记,颜色差异化展示

  3. 任务编辑功能:长按任务实现内容修改、重新编辑

  4. 动画优化:新增任务新增、删除、状态切换过渡动画

  5. 滑动删除:实现列表项右滑删除交互,优化操作体验

  6. 分类管理:新增任务分类标签,实现多维度分类筛选

八、项目总结

本文基于 HarmonyOS NEXT API23 最新规范,从零开发了一款功能完整、代码规范、UI精美的Todo待办事项应用。全程覆盖Interface数据结构定义、@State响应式状态管理、ArkUI核心组件、ForEach循环渲染、条件渲染、模块化组件封装等鸿蒙入门核心刚需知识点。

该项目完美诠释了鸿蒙数据驱动UI的开发思想,代码结构清晰、逻辑严谨、可扩展性强,零基础可快速上手,非常适合作为鸿蒙入门实训、课程设计、毕设练手、技术发文的标杆项目,助力开发者夯实ArkUI声明式开发基础。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐