ArkUI 全记录03:跟着百万博主从零手写 TodoApp 的完整心路历程
ArkUI TodoApp 开发摘要 本教程手把手教你用ArkUI实现一个功能完整的待办事项应用,涵盖以下核心开发要点: 环境搭建 下载安装DevEco Studio 5.0 配置HarmonyOS SDK(API 23) 创建Empty Ability项目 核心实现 使用interface定义数据结构 @Component构建组件 @State实现状态管理 Column/Row布局设计 Text
🎯 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 配置步骤:
- 打开 DevEco Studio → 点击 Configure → Settings
- 搜索 SDK → 选择 HarmonyOS SDK 路径
- 勾选 API 23(HarmonyOS NEXT) → 点击 Apply
- 等待下载完成(大约需要 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)
}
这里我学到了几个关键点:
- Checkbox 搭配
.select()控制选中状态 —— 不是用checked,而是用select .decoration()实现文字删除线 —— 这是 ArkUI 的文本装饰,LineThrough就是删除线效果- 三元表达式做条件样式 ——
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/
更多推荐
所有评论(0)