🎯 ArkUI 全记录:跟着百万博主从零手写 TodoApp 的完整心路历程


一、这篇教程在教什么?

先说说这个 Tutorial 的背景。博主的文章是一个待办事项(Todo List)应用教程,用纯 ArkTS + ArkUI 实现。

我当时在想:Todo 应用也太简单了吧? 但跟着写完之后我才明白,它包含了 ArkUI 开发的 所有核心概念

核心概念 在 Todo 里对应什么
声明式 UI @Component 描述界面长什么样
状态管理 @State 让数据变了 UI 自动刷新
常用组件 TextInput(输入框)、Checkbox(勾选框)、Button
布局 Column(纵向)、Row(横向)、List(列表)
列表渲染 ForEach 循环渲染任务列表
事件处理 点击添加、勾选完成、点击删除
动画 animateTo 让增删过渡更丝滑
数据筛选 按"全部/进行中/已完成"过滤
数据统计 实时显示总数和完成数

一句话总结:别看 Todo 小,五脏俱全。 通过它你能搞懂 ArkUI 90% 的日常开发场景。


二、我的环境搭建过程(附截图步骤)

2.1 下载 DevEco Studio

第一关:装开发工具

我去华为开发者官网下载了 DevEco Studio 5.0(最新版),安装包大概 1.2 GB,下载了大概 10 分钟。

📸 步骤截图说明

[步骤 1] 打开华为开发者联盟官网 → 找到 DevEco Studio 下载页
[步骤 2] 选择 Windows 版本 → 点击下载
[步骤 3] 双击安装包 → 一路 Next → 安装完成

2.2 配置 SDK(我卡得最久的一步)

装完 DevEco Studio,第一件事是配置 SDK。这里我踩了个大坑——

第一次打开 IDE,它会自动提示下载 SDK,但下载进度条走到一半就停了,我以为是卡住了,直接关掉了窗口。结果后面怎么都编译不通过。

后来才知道:SDK 下载不能中断,而且最好挂个代理或者晚上下载(速度会快一些)。

正确的 SDK 配置步骤:

  1. 打开 DevEco Studio → 点击 ConfigureSettings
  2. 搜索 SDK → 选择 HarmonyOS SDK 路径
  3. 勾选 API 23(HarmonyOS NEXT) → 点击 Apply
  4. 等待下载完成(大约需要 15-30 分钟,看你网速)

📸 SDK 配置截图说明

[截图] DevEco Studio 的 SDK Manager 界面
- 左侧导航:Appearance & Behavior → System Settings → HarmonyOS SDK
- 右侧:勾选 API 23 → Apply 按钮

2.3 创建项目

SDK 装好后,创建项目:

1. 点击 "Create HarmonyOS Project"
2. 选模板:选择 "Empty Ability"(空的页面模板)
3. 项目信息:
   - Project Name: TodoApp
   - Bundle Name: com.example.todoapp
   - Save Location: 自己选一个文件夹
4. 选 API 版本:API 23(ArkTS)
5. 点击 Finish

📸 创建项目截图说明

[截图] Create Project 界面
- 模板选择 Empty Ability
- 项目名称输入 TodoApp
- API 版本选择 23

第一次创建项目,IDE 会下载一些依赖,等待右下角的进度条走完就行。

2.4 认识项目结构(小白必看!)

项目创建完成后,左侧会出现这样的目录结构(我当时看了半天才明白):

TodoApp/
├── AppScope/                          # 应用配置
│   └── app.json5                      # 应用名字、图标等
├── entry/                             # 主模块(我们的代码在下面)
│   └── src/main/
│       ├── ets/                       # ★ ArkTS 源代码(重点!)
│       │   ├── entryability/          # 应用入口(生命周期)
│       │   └── pages/                 # 页面文件夹
│       │       └── Index.ets          # ★ 我们要改的文件
│       └── resources/                 # 图片、字符串等资源
├── build-profile.json5                # 构建配置
├── oh-package.json5                   # 依赖管理
└── hvigorfile.ts                      # 构建脚本

新手只需要记住一件事: 我们 99% 的代码都写在 entry/src/main/ets/pages/Index.ets 这个文件里。

💡 我的理解.ets 就是 ArkTS 文件的后缀名,相当于前端的 .js.tsx,是鸿蒙原生开发的代码文件。


三、从零开始写代码(跟着博主一步步来)

3.1 第一步:定义数据结构 —— 认识 interface

打开 Index.ets,发现里面已经有了一些示例代码,但我们要全部删掉重写。

博主的第一步是定义待办事项的数据结构

// 定义待办事项的数据结构
interface TodoItem {
  id: number;         // 唯一标识 —— 区分不同的任务
  text: string;       // 任务内容 —— 用户输入的文字
  completed: boolean; // 是否完成 —— true=已完成,false=未完成
  createdAt: string;  // 创建时间 —— 记录什么时候创建的
}

🤔 我的理解interface 就是"数据模板"。它告诉程序:“每个待办事项都必须有 id、text、completed、createdAt 这四个字段”。

想象一下,它就像考试答题卡的填涂框—— 规定了每个位置填什么内容,填错了就识别不了。number 是数字,string 是文本,boolean 是 true/false。

小白的额外发现? 可以标记可选属性,比如:

interface TodoItem {
  id: number;
  text: string;
  completed: boolean;
  createdAt: string;
  priority?: number;   // 问号表示"可以有,也可以没有"
  tags?: string[];     // 可选的标签列表
}

这样定义后,每个待办事项可以有自己的优先级,但不是必须的。


3.2 第二步:创建组件 —— 认识 @Component 和 @State

接下来是最重要的一步:创建主组件

@Entry                          // 入口标记(这个页面是应用首页)
@Component                      // 声明这是一个组件
struct TodoApp {                // 组件名叫 TodoApp
  @State todos: TodoItem[] = [];     // 所有待办事项
  @State newTodoText: string = '';   // 输入框的文字
  @State nextId: number = 1;         // 下一个任务的 ID
  @State filter: number = 0;         // 筛选条件:0-全部 1-进行中 2-已完成

  build() {
    // 这里写界面
  }
}

🚨 我踩的坑:我第一次写的时候,把 @State 写成了 @state(小写 s),结果编译直接报错。装饰器必须严格区分大小写!

还有一次我忘了写 @Entry,结果模拟器黑屏。@Entry 是告诉系统"该从哪个页面启动"。

@State 到底是什么?

博主文章里花了很大篇幅解释 @State,我一开始没看懂,后来用了一个比喻才明白:

@State 就像一个"智能监控摄像头" —— 它盯着变量的值,一旦值变了,它就大喊"来人啊!值变了!",然后 ArkUI 框架就会重新渲染界面。

所以当你勾选一个复选框时,todos[index].completed 变成了 true,@State 检测到变化,UI 自动刷新,文字出现了删除线。

这就是"数据驱动 UI"的精髓 —— 你只需要改数据,UI 自己会变!


3.3 第三步:搭页面布局 —— Column + Row + Text

博主的下一步是搭页面的整体框架。ArkUI 的布局主要用 Column(纵向排列)Row(横向排列)

我按照博主的代码,在 build() 方法中写:

build() {
  Column() {                          // ← 最外层:垂直排列
    // 1. 标题
    Text('我的待办')
      .fontSize(28)
      .fontWeight(FontWeight.Bold)
      .width('100%')
      .textAlign(TextAlign.Start)
      .padding({ top: 20, bottom: 10 })

    // 2. 统计信息
    Text(this.getStatisticsText())   // 调用方法获取统计文字
      .fontSize(14)
      .fontColor('#999')
      .width('100%')
      .padding({ bottom: 15 })

    // 3. 输入区域(输入框 + 添加按钮)
    Row() {                          // ← Row:横向排列
      TextInput({
        placeholder: '请输入待办事项...',
        text: this.newTodoText
      })
        .width('85%')
        .height(48)
        .backgroundColor('#F5F5F5')
        .borderRadius(8)
        .onChange((value: string) => {
          this.newTodoText = value;   // 输入变化时更新状态
        })
        .onSubmit(() => {
          this.addTodo();            // 按回车提交
        })

      Button('添加')
        .width('13%')
        .height(48)
        .backgroundColor('#6366F1')
        .borderRadius(8)
        .fontColor('#FFFFFF')
        .onClick(() => {
          this.addTodo();
        })
    }
    .width('100%')
    .padding({ bottom: 15 })

    // 4. 列表区域(后面补充)
    // 5. 底部按钮(后面补充)
  }
  .padding(20)
  .width('100%')
  .height('100%')
  .backgroundColor('#FFFFFF')
}

🤔 我的理解

  • Column() 相当于 HTML 的 flex-direction: column,里面的东西从上往下排
  • Row() 相当于 flex-direction: row,里面的东西从左往右排
  • .width('100%') 是链式调用,ArkUI 里几乎所有属性都能这样"点"出来

ArkUI 和 CSS 的一个关键区别:ArkUI 是用方法链设置样式(.fontSize(28).fontColor('#333')),而 CSS 是写键值对({font-size: 28px; color: #333})。


3.4 第四步:写业务逻辑 —— 增删改查

写完界面布局,接下来是实现核心功能。我按照博主给的代码,在 build() 方法外面添加了这些方法:

// ===== 添加待办 =====
addTodo() {
  const text = this.newTodoText.trim();
  if (text === '') return;  // 空输入不处理

  this.todos.push({
    id: this.nextId++,
    text: text,
    completed: false,
    createdAt: new Date().toLocaleDateString()
  });
  this.newTodoText = '';  // 清空输入框
}

// ===== 切换完成状态 =====
toggleTodo(id: number) {
  const index = this.todos.findIndex(todo => todo.id === id);
  if (index >= 0) {
    this.todos[index].completed = !this.todos[index].completed;
  }
}

// ===== 删除待办 =====
deleteTodo(id: number) {
  this.todos = this.todos.filter(todo => todo.id !== id);
}

// ===== 获取筛选后的列表 =====
getFilteredTodos(): TodoItem[] {
  if (this.filter === 0) return this.todos;
  if (this.filter === 1) return this.todos.filter(todo => !todo.completed);
  return this.todos.filter(todo => todo.completed);
}

// ===== 统计信息 =====
getStatisticsText(): string {
  const total = this.todos.length;
  const completed = this.todos.filter(t => t.completed).length;
  return `${total} 项,已完成 ${completed}`;
}

// ===== 清除已完成 =====
clearCompleted() {
  this.todos = this.todos.filter(todo => !todo.completed);
}

🚨 我踩的大坑(数组操作)

第一次写删除功能时,我用了 this.todos.splice(index, 1) —— 结果 UI 没刷新!

原因:@State 装饰的数组,用 push() 添加能用是因为 ArkUI 对 push 做了特殊处理。但 splice 不行。删除必须用 重新赋值 的方式:

✅ 正确:this.todos = this.todos.filter(todo => todo.id !== id)
❌ 错误:this.todos.splice(index, 1)

这是新手最容易踩的坑之一,博主文章里专门强调了这一点,还好我看了。


3.5 第五步:用 @Builder 封装复用 UI

写到筛选标签的时候,我发现"全部"“进行中”"已完成"三个标签的样式很像,只是文字和值不同。

博主在这里用了 @Builder —— ArkUI 的 UI 复用装饰器:

@Builder
createFilterTab(filterValue: number, label: string) {
  Text(label)
    .fontSize(14)
    .fontColor(this.filter === filterValue ? '#6366F1' : '#999')
    .fontWeight(this.filter === filterValue ? FontWeight.Bold : FontWeight.Normal)
    .padding({ top: 6, bottom: 6, left: 16, right: 16 })
    .backgroundColor(this.filter === filterValue ? '#EEF2FF' : '#F5F5F5')
    .borderRadius(16)
    .onClick(() => {
      this.filter = filterValue;
    })
}

然后在 build() 里这样调用:

Row() {
  this.createFilterTab(0, '全部')
  this.createFilterTab(1, '进行中')
  this.createFilterTab(2, '已完成')
}
.width('100%')
.justifyContent(FlexAlign.SpaceEvenly)

🤔 我的理解@Builder 就像定义了一个"UI 函数",传不同的参数就能生成不同的 UI。如果不用 @Builder,这三个标签的 15 行代码我要复制粘贴 3 次,那就是 45 行重复代码。@Builder 一下子缩减到了 15 行。


3.6 第六步:用 ForEach 渲染列表

接下来是最"爽"的一步 —— 让列表真正显示出来!

List({ space: 10 }) {        // space=10 是列表项间距
  ForEach(this.getFilteredTodos(), (todo: TodoItem) => {
    ListItem() {
      this.todoItemView(todo)  // 调用 @Builder 渲染每个待办
    }
  })
}
.width('100%')
.layoutWeight(1)  // 填充剩余空间

💡 特别注意:博主的代码里用的是 ForEach(F大写),而不是 JavaScript 的 forEach(f小写)。两者区别很大:

  • ForEach(大写)是 ArkUI 组件,用来渲染列表
  • forEach(小写)是 JavaScript 数组方法,用来遍历数组

我第一次写错了,编译报错"找不到符号 ForEach"…… 检查了半天才发现是大写小写的问题。

结合 @Builder 写的待办项 UI:

@Builder
todoItemView(todo: TodoItem) {
  Row() {
    Checkbox()
      .select(todo.completed)
      .selectedColor('#6366F1')
      .onChange(() => {
        this.toggleTodo(todo.id);
      })

    Text(todo.text)
      .fontSize(16)
      .fontColor(todo.completed ? '#CCC' : '#333')
      .decoration({
        type: todo.completed ?
          TextDecorationType.LineThrough : TextDecorationType.None
      })
      .layoutWeight(1)
      .padding({ left: 8 })

    Text(todo.createdAt)
      .fontSize(12)
      .fontColor('#CCC')

    Button() {
      Text('✕').fontSize(14).fontColor('#FF6B6B')
    }
    .backgroundColor('transparent')
    .width(30)
    .height(30)
    .onClick(() => {
      this.deleteTodo(todo.id);
    })
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#FAFAFA')
  .borderRadius(10)
  .alignItems(VerticalAlign.Center)
}

这里我学到了几个关键点:

  1. Checkbox 搭配 .select() 控制选中状态 —— 不是用 checked,而是用 select
  2. .decoration() 实现文字删除线 —— 这是 ArkUI 的文本装饰,LineThrough 就是删除线效果
  3. 三元表达式做条件样式 —— todo.completed ? '#CCC' : '#333' 是 ArkTS 的标准语法

3.7 第七步:加上动画效果

博主在文章最后加上了 animateTo 动画——这算是一个"加分项":

// 添加时带动画
addTodo() {
  const text = this.newTodoText.trim();
  if (text === '') return;

  animateTo({ duration: 300 }, () => {
    this.todos.push({
      id: this.nextId++,
      text: text,
      completed: false,
      createdAt: new Date().toLocaleDateString()
    });
  });
  this.newTodoText = '';
}

// 删除时带动画
deleteTodo(id: number) {
  animateTo({ duration: 200 }, () => {
    this.todos = this.todos.filter(todo => todo.id !== id);
  });
}

🤔 我的理解animateTo 是 ArkUI 的显式动画 API。它的工作原理是:把状态变更放在回调函数里,ArkUI 会自动计算"变化前的 UI"到"变化后的 UI"之间的差值,然后在这两个状态之间插帧过渡

比如删除一个任务时,animateTo 会让这个任务先有一个"淡出并缩小"的动画效果,而不是"啪"一下突然消失。用户体验会好很多。


3.8 运行成功的那一刻!

写完所有代码后,我点击了右上角的 运行按钮(▶️),选择了 P40 Pro 模拟器

等待大概 1 分钟(模拟器启动比较慢),屏幕上终于出现了我的 TodoApp!

📸 运行成功截图说明

[HUAWEI P40 Pro 模拟器截图]
┌─────────────────────────────────┐
│  我的待办                        │
│  共 3 项,已完成 1 项            │
│                                 │
│  [写今天的作业...         ] [添加]│
│                                 │
│  ○ 全部  ○ 进行中  ● 已完成     │
│                                 │
│  ☑ 学习 ArkUI      2025/01/15 ✕ │
│  ☐ 写 TodoApp      2025/01/15 ✕ │
│  ☐ 发布博客文章    2025/01/16 ✕ │
│                                 │
│  [    清除已完成    ]            │
└─────────────────────────────────┘

那一刻真的很有成就感! 虽然只是一个简单的 TodoApp,但这是我亲手写了每一行代码、从 0 到 1 构建出来的应用。

我测试了所有功能:

  • ✅ 输入文字 → 点击添加 → 任务出现
  • ✅ 点击复选框 → 文字出现删除线
  • ✅ 点击 ✕ 按钮 → 任务被删除(带动画)
  • ✅ 点击筛选标签 → 只显示对应状态的任务
  • ✅ 点击"清除已完成" → 所有完成的任务消失
  • ✅ 统计信息实时更新

全部工作正常!


四、我踩过的坑汇总(新手避坑指南)

下面是我在学习过程中踩过的所有坑,每一行都是血泪教训

分类 坑的描述 正确做法 我的血泪史
🚨 装饰器 @State 写成 @state 装饰器首字母必须大写 编译报错了 10 分钟才找到原因
🚨 数组操作 splice() 删除元素 filter() 重新赋值 删了但 UI 不刷新
🚨 ForEach 写成 forEach(小写) 必须大写 ForEach 编译报错"找不到符号"
🚨 TextInput 没传 text 参数 TextInput({text: this.text}) 输入了但状态不更新
🚨 导包 忘写 import ArkTS 自动导入 其实不用手动 import 组件 😅
🚨 模拟器 SDK 下载中断 挂代理 + 一次性下完 项目构建失败,重装了两次
🚨 build() build() 里写业务逻辑 build() 只写 UI 代码一长就很难维护
🚨 颜色格式 #FFF(三位简写) 必须写 #FFFFFF(六位) 颜色不生效

五、学完这个项目后的核心收获

5.1 我理解的 ArkUI 声明式语法

学完 TodoApp,我对 ArkUI 最核心的 声明式语法 有了直观认识:

┌─────────────────────────────────────────────┐
│  声明式语法 = "描述结果,而不是描述过程"        │
│                                              │
│  传统写法(命令式):                          │
│    button.setColor('blue')                   │
│    label.setText('Hello')                    │
│    view.addChild(button)                     │
│                                              │
│  ArkUI(声明式):                            │
│    Button('Hello')                           │
│      .backgroundColor('blue')                │
│      .fontSize(16)                           │
│                                              │
│  区别:声明式直接描述"界面应该长什么样",        │
│  而不是"先创建按钮,再设置颜色,再添加到视图"    │
└─────────────────────────────────────────────┘

5.2 我理解的 @State 核心机制

用户操作(点击Checkbox)
      ↓
触发 onChange 事件
      ↓
更新 @State 变量(todos[index].completed = true)
      ↓
ArkUI 框架监听到状态变化
      ↓
重新执行 build() 生成新 UI
      ↓
界面自动刷新(文字出现删除线)

这就是 “数据驱动 UI” 的完整链路。我现在理解了为什么叫"驱动"—— 数据是发动机,UI 只是被带动的轮子。

5.3 我理解的常用组件关系

页面结构:
┌─ Column(纵向容器)──────┐
│  ├─ Text(标题)        │
│  ├─ Row(横向容器)      │
│  │  ├─ TextInput(输入) │
│  │  └─ Button(按钮)    │
│  ├─ Row(筛选栏)        │
│  │  └─ Text × 3(标签)  │
│  ├─ List(可滚动列表)    │
│  │  └─ ListItem × N     │
│  │     └─ Row            │
│  │        ├─ Checkbox    │
│  │        ├─ Text(内容) │
│  │        └─ Button(删) │
│  └─ Button(清除已完成)   │
└─────────────────────────┘

六、给其他小白的建议 + 下一步学习路线

6.1 给新手的三条真心建议

1️⃣ 不要怕报错,报错是老师

我写这个项目至少报错了 20 次,但每次报错我都把错误信息复制到百度/CSDN搜一下,基本上都能找到答案。错误信息是最好的学习材料。

2️⃣ 先照抄,再理解,后创造

我的学习路径是:先把博主的代码一字不差地敲一遍 → 跑起来了 → 试着改几个参数看效果 → 理解为什么要这样写 → 自己加新功能。

不要试图第一次就理解所有代码,先让它跑起来,信心就有了。

3️⃣ 把 @State 理解透,就学会了 ArkUI 的一半

ArkUI 和传统 Android 开发最大的区别就是 状态驱动。搞懂 @State 怎么用、什么时候 UI 会刷新,你就能解释 ArkUI 的大部分行为了。

本文首发于 CSDN,作者:一名正在学习鸿蒙开发的小白
参考教程:@AHuiHatedebug 的【鸿蒙原生应用开发–ArkUI–003】系列

官方文档:HarmonyOS 应用开发文档

  • 开发者社区:华为开发者论坛
  • 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net/

Logo

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

更多推荐