ArkUI新手小白学习手写TodoApp全过程03
# 🎯 小白学鸿蒙 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 → 点击 **Configure** → **Settings**
2. 搜索 **SDK** → 选择 HarmonyOS SDK 路径
3. 勾选 **API 23(HarmonyOS NEXT)** → 点击 Apply
4. 等待下载完成(大约需要 15-30 分钟,看你网速)
> 📸 **SDK 配置说明**:
> ```
> - 左侧导航: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`,发现里面已经有了一些示例代码,但我们要全部删掉重写。
博主的第一步是定义**待办事项的数据结构**:
```typescript
// 定义待办事项的数据结构
interface TodoItem {
id: number; // 唯一标识 —— 区分不同的任务
text: string; // 任务内容 —— 用户输入的文字
completed: boolean; // 是否完成 —— true=已完成,false=未完成
createdAt: string; // 创建时间 —— 记录什么时候创建的
}
```
> **🤔 我的理解**:`interface` 就是"数据模板"。它告诉程序:"每个待办事项都必须有 id、text、completed、createdAt 这四个字段"。
>
> 想象一下,它就像**考试答题卡的填涂框**—— 规定了每个位置填什么内容,填错了就识别不了。`number` 是数字,`string` 是文本,`boolean` 是 true/false。
**小白的额外发现**:`?` 可以标记可选属性,比如:
```typescript
interface TodoItem {
id: number;
text: string;
completed: boolean;
createdAt: string;
priority?: number; // 问号表示"可以有,也可以没有"
tags?: string[]; // 可选的标签列表
}
```
这样定义后,每个待办事项可以有自己的优先级,但不是必须的。
---
### 3.2 第二步:创建组件 —— 认识 @Component 和 @State
接下来是最重要的一步:**创建主组件**。
```typescript
@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()` 方法中写:
```typescript
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()` 方法外面添加了这些方法:
```typescript
// ===== 添加待办 =====
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 复用装饰器:
```typescript
@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()` 里这样调用:
```typescript
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 渲染列表
接下来是最"爽"的一步 —— **让列表真正显示出来!**
```typescript
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:
```typescript
@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` 动画——这算是一个"加分项":
```typescript
// 添加时带动画
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](https://blog.csdn.net/AHuiHatedebug) 的【鸿蒙原生应用开发--ArkUI--003】系列*
官方文档:HarmonyOS 应用开发文档
- 开发者社区:华为开发者论坛
- 欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net/
---
更多推荐



所有评论(0)