用ArkTS做一个待办清单App
本文手把手教你用鸿蒙ArkTS开发一个待办清单App。首先介绍了项目效果:点击待办事项可切换完成状态,显示不同图标和样式。核心使用了Text、Column、Row、Image、List等组件,重点讲解了@State状态管理的响应式特性。文章分步骤实现:1)搭建主框架,使用ForEach循环渲染待办列表;2)重点开发EventText子组件,通过@State变量控制完成状态,实现图标切换、文字删除线
咱们来做一个鸿蒙待办清单 App —— 从零开始,手把手教你
说实话,待办清单(Todo List)这个东西,基本上是所有开发框架入门的"第一课"。你在学网页开发的时候写过,学 Android 的时候写过,现在轮到鸿蒙了,还是它。但别小看这个案例,虽然逻辑不复杂,但它能帮你搞明白 ArkTS 声明式 UI 里几个最核心的东西:@State 状态管理、组件嵌套、列表渲染、事件处理。
这篇文章就一步一步来,带你把这个待办清单做出来。放心,每一步我都会解释清楚,不会跳过任何代码。
先看看最终效果
做之前先有个概念,我们最终要实现的效果是这样的:
页面上方有个"待办"标题,下面是一个列表,列表里有几条待办事项,比如"ArkTS基础语法"、“ArkTS案例讲解"之类的。每条事项前面有一个图标,后面也有一个图标。当你点击某一条的时候,图标会变,文字会加删除线,整行背景颜色也会变,表示"这件事已经做完了”。再点一下,又恢复原样。
就是这么一个简单的交互,但里面用到了好几个重要的 ArkTS 知识点。
用到哪些组件?
先别急着写代码,我们先认识一下这次要用的几个基础组件:
- Text 组件:就是显示文字的,跟 HTML 里的 span 差不多
- Column 组件:垂直布局容器,里面放的东西会从上到下排列
- Row 组件:水平布局容器,里面放的东西会从左到右排列
- Image 组件:显示图片的,这里我们用它来显示图标
- List / ListItem 组件:列表容器,跟 HTML 里的 ul/li 是一个意思
- ForEach:循环渲染,用来把数组里的数据变成一组 UI 组件
这里面最关键的是 @State 装饰器——它是 ArkTS 状态管理的核心。被 @State 修饰的变量一旦发生变化,UI 会自动刷新。这个概念如果你之前接触过 Vue 的响应式数据,或者 React 的 useState,应该会觉得很熟悉。
开干!先搭主框架
打开 DevEco Studio,创建一个新项目(Empty Ability 就行),然后找到 pages/Index.ets 这个文件,把里面的内容全部删掉,我们从零开始写。
首先,写最外层的入口组件:
@Entry
@Component
struct Index {
// 待办事项文本内容,用一个 string 数组来存
private event: string[] =
['ArkTS基础语法', 'ArkTS案例讲解', 'ArkTS案例练习', 'ArkTS UI语法', 'ArkTS界面开发', 'ArkTS逻辑开发']
build() {
// 整个页面用一个 Column 来做垂直布局
Column() {
// 标题
Text('待办')
.font({ size: 30, weight: FontWeight.Bold })
.width('90%').textAlign(TextAlign.Start)
// 列表
List({ space: 10 }) {
// 用 ForEach 循环创建列表项
ForEach(this.event, (item: string) => {
ListItem() {
// 每一项调用自定义的 EventText 组件
EventText({ content: item })
}
})
}
.width('100%').height('100%')
.alignListItem(ListItemAlign.Center)
}
.height('100%')
.width('100%')
}
}
来一行一行看:
@Entry 这个装饰器告诉系统,这是一个页面的入口组件。一个页面只有一个 @Entry,就像一个 HTML 文件只有一个 body。
@Component 表示这是一个自定义组件。在 ArkTS 里,你想封装一个可复用的 UI 组件,就加上这个装饰器。
private event: string[] = [...] 这里我们定义了一个私有变量,存了 6 条待办事项的文本。注意,这里没有用 @State,因为这个数组本身不会变化(我们不会动态添加或删除待办事项,只是切换已完成/未完成的状态)。真正会变化的状态,我们放在子组件里。
build() 是每个组件必须有的方法,用来描述这个组件长什么样。你把 UI 结构写在里面, ArkTS 的渲染引擎就会把它画到屏幕上。
Column() 是整个页面的外层容器。Column 会让里面的子组件从上到下排列,所以标题在上面,列表在下面。
Text('待办') 就是一个文本标签,显示"待办"两个字。后面的 .font() 设置字号和粗体,.width('90%') 让它占页面宽度的 90%,.textAlign(TextAlign.Start) 让文字左对齐。这样标题不会紧贴屏幕左边缘,留了一点呼吸空间。
List({ space: 10 }) 是列表容器,space: 10 表示每个列表项之间有 10vp 的间距。vp 是鸿蒙的虚拟像素单位,跟 dp 差不多意思。
ForEach(this.event, (item: string) => {...}) 这就是循环渲染。它会遍历 this.event 数组里的每一项,对每一项执行里面的回调函数,生成一个 ListItem。这跟 JavaScript 的 array.map() 是一个道理。
EventText({ content: item }) 这里我们调用了自定义的 EventText 组件,把当前循环的文本内容通过 content 参数传进去。这个组件我们接下来写。
.alignListItem(ListItemAlign.Center) 让每个列表项在列表中居中显示。
好,主框架就是这样,不复杂对吧?最核心的逻辑其实在那个子组件 EventText 里。
重点来了:写子组件 EventText
这个子组件负责渲染每一个待办事项,并且处理点击切换状态的所有逻辑。
@Component
struct EventText {
// 状态变量,控制当前事项是否已完成
@State radio: boolean = false
// 从父组件传进来的文本内容
private content?: string
build() {
Row() {
// 左侧图标:根据状态显示不同的图片
if (this.radio) {
Image($r('app.media.ic_filled'))
.width(25)
} else {
Image($r('app.media.ic_security'))
.width(25)
}
// 中间文本:根据状态改变字号、删除线、透明度
Text(this.content)
.fontSize(this.radio ? 22 : 23)
.decoration({
type: this.radio ? TextDecorationType.LineThrough : TextDecorationType.None
})
.opacity(this.radio ? 0.5 : 1)
// 右侧图标:同样根据状态切换
Image($r(this.radio ? 'app.media.ic_filled' : 'app.media.ic_security'))
.width(this.radio ? 24 : 25)
.height(this.radio ? 24 : 25)
}
.onClick(() => {
this.radio = !this.radio
})
.width('90%')
.height(45)
.padding(5)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(this.radio ? '#dddddd' : '#eeeeee')
}
}
这段代码值得好好说说,因为它包含了 ArkTS 里好几个关键概念。
@State 是灵魂
@State radio: boolean = false 这一行是整个案例的核心。
radio 是一个布尔值,初始是 false,表示这条待办事项还没完成。关键是 @State 这个装饰器——它告诉 ArkTS 框架:“这个变量是一个状态变量,你帮我盯着它,它一变,你就把用到它的 UI 全部重新渲染。”
所以当我们后面在 onClick 里写 this.radio = !this.radio 的时候,框架检测到 radio 变了,就会自动重新执行 build() 方法,把新的状态反映到界面上。图标换了、文字加了删除线、背景色变了——这些都是因为 @State 触发的自动刷新。
如果你去掉 @State,直接写 radio: boolean = false,那点击之后数据变了但界面不会更新,你就会看到"点了没反应"的 bug。
用 if 语句切换图标
if (this.radio) {
Image($r('app.media.ic_filled'))
.width(25)
} else {
Image($r('app.media.ic_security'))
.width(25)
}
这里用了 ArkTS 的条件渲染。当 radio 为 true 时显示已完成的图标(ic_filled),为 false 时显示未完成的图标(ic_security)。
$r('app.media.ic_filled') 这种写法是 ArkTS 的资源引用方式。app.media 表示应用的 media 资源目录,ic_filled 是图标文件名。你需要把对应的图标文件放到项目的 resources/base/media/ 目录下。
这里提醒一下,图标文件你需要自己准备两个:
ic_filled.png(或 .svg)—— 已完成状态的图标,比如一个打勾的圆圈ic_security.png(或 .svg)—— 未完成状态的图标,比如一个空心的圆圈
文本的三种状态变化
Text(this.content)
.fontSize(this.radio ? 22 : 23)
.decoration({
type: this.radio ? TextDecorationType.LineThrough : TextDecorationType.None
})
.opacity(this.radio ? 0.5 : 1)
这段代码让文本在"已完成"和"未完成"两种状态下看起来不一样:
- 字号:完成后从 23 变成 22,稍微小一点。这个变化很微妙,不是必须的,但给人一种"这个事项已经不那么重要了"的感觉。
- 删除线:这是最明显的视觉变化。
TextDecorationType.LineThrough就是给文字加一条横线穿过中间,跟 CSS 的text-decoration: line-through一模一样。 - 透明度:完成后变成 0.5,也就是半透明。这样跟旁边的未完成事项一对比,视觉上就能很清楚地看出哪些做完了,哪些还没做。
这三个效果组合在一起,用户体验就很好了——你一眼就能看出哪些事项已经处理过了。
右侧图标用了一个小技巧
Image($r(this.radio ? 'app.media.ic_filled' : 'app.media.ic_security'))
.width(this.radio ? 24 : 25)
.height(this.radio ? 24 : 25)
注意看这里跟左侧图标的写法不一样。左侧用了 if...else 来切换整个 Image 组件,右侧则是在同一个 Image 里动态切换资源路径。两种写法都能实现效果,这里主要是展示不同的写法。
右侧图标在完成状态下宽高都是 24,未完成时是 25。这个 1vp 的差别可能肉眼看不太出来,但它说明了一个事情:你可以用三元表达式根据状态来动态调整任何样式属性。
Row 的布局设置
Row() {
// ... 左图标、文本、右图标
}
.onClick(() => {
this.radio = !this.radio
})
.width('90%')
.height(45)
.padding(5)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(this.radio ? '#dddddd' : '#eeeeee')
Row() 是一个水平布局容器,里面的三个元素(左图标、文本、右图标)会从左到右排列。
.onClick() 绑定了点击事件。点击整行任何位置都会触发,把 radio 取反——原来是 false 就变成 true,原来是 true 就变成 false。这就是切换的核心逻辑,非常简洁。
.width('90%') 让每一行占页面 90% 的宽度,两边留白。
.height(45) 固定每行高度为 45vp,这样所有行的高度一致,看起来整齐。
.padding(5) 内边距 5vp,让内容不会紧贴行边缘。
.borderRadius(10) 圆角 10vp,让行看起来不那么死板,有点卡片的感觉。
.justifyContent(FlexAlign.SpaceBetween) 这个很关键。它让 Row 里的三个子元素(左图标、文本、右图标)分别贴到行的最左边、中间、最右边。如果没有这个设置,三个元素会挤在一起。
.backgroundColor(this.radio ? '#dddddd' : '#eeeeee') 背景色也根据状态变化。未完成时是 #eeeeee(浅灰),完成时是 #dddddd(稍深一点的灰)。这个颜色差异虽然不大,但跟文本的透明度变化配合起来,完成和未完成的区别就很明显了。
完整代码
把上面的两个组件拼在一起,完整的 Index.ets 长这样:
@Entry
@Component
struct Index {
// 待办事项文本内容 string 数组
private event: string[] =
['ArkTS基础语法', 'ArkTS案例讲解', 'ArkTS案例练习', 'ArkTS UI语法', 'ArkTS界面开发', 'ArkTS逻辑开发']
build() {
// 乘法表的布局展示
Column() {
// 标题
Text('待办')
.font({ size: 30, weight: FontWeight.Bold })
.width('90%').textAlign(TextAlign.Start)
// 列表
List({ space: 10 }) {
// 利用循环创建列表项
ForEach(this.event, (item: string) => {
ListItem() {
// 调用 string 数组为列表项填充文本内容
EventText({ content: item })
}
})
}
.width('100%').height('100%')
.alignListItem(ListItemAlign.Center)
}
.height('100%')
.width('100%')
}
}
@Component
struct EventText {
// 状态变量,改变显式状态
@State radio: boolean = false
private content?: string
build() {
Row() {
// 用 if 语句配合状态变量实现点击切换图标
if (this.radio) {
Image($r('app.media.ic_filled'))
.width(25)
} else {
Image($r('app.media.ic_security'))
.width(25)
}
// 用变量填充文本内容,用状态变量设置点击事件中各属性的变化,从而更新视图状态
Text(this.content)
.fontSize(this.radio ? 22 : 23)
.decoration({
type: this.radio ? TextDecorationType.LineThrough : TextDecorationType.None
})
.opacity(this.radio ? 0.5 : 1)
Image($r(this.radio ? 'app.media.ic_filled' : 'app.media.ic_security'))
.width(this.radio ? 24 : 25)
.height(this.radio ? 24 : 25)
}
.onClick(() => {
this.radio = !this.radio
})
.width('90%')
.height(45)
.padding(5)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.backgroundColor(this.radio ? '#dddddd' : '#eeeeee')
}
}
几个你可能踩的坑
1. 图标文件找不到
如果你运行后看到图标位置是空白的或者报错,大概率是 ic_filled 和 ic_security 这两个图标文件没有放到正确的位置。它们应该放在 resources/base/media/ 目录下。图标格式可以是 png、jpg、svg,都可以。
2. @State 忘了加
如果你发现点击之后界面完全没反应,但逻辑上 radio 确实变了,那检查一下是不是 @State 装饰器漏了。没有 @State,ArkTS 不知道要监听这个变量的变化,UI 就不会刷新。
3.ForEach 的 key 问题
如果你以后在 ForEach 里遇到了列表渲染异常(比如删除一项后显示错乱),那是因为 ForEach 需要一个唯一的 key 来追踪每个列表项。我们这个案例因为数组是固定的不会变化,所以没加 key 也能正常运行。但在实际项目中,建议给 ForEach 加上第三个参数作为 key:
ForEach(this.event, (item: string) => {
ListItem() {
EventText({ content: item })
}
}, (item: string) => item)
第三个参数是一个 key 生成函数,返回一个唯一值来标识每一项。
4. 私有变量 vs 状态变量
Index 组件里的 event 数组用的是 private,不是 @State。因为我们的待办事项列表是固定的,不会动态增删。如果你以后想实现"添加新待办"的功能,那就要把 event 改成 @State 修饰的数组,这样往数组里 push 新元素的时候 UI 才会更新。
总结一下这个案例里学到的东西
| 概念 | 用在哪 | 说明了什么 |
|---|---|---|
@Entry |
Index 组件 | 标记页面入口 |
@Component |
Index、EventText | 定义自定义 UI 组件 |
@State |
EventText.radio | 状态变化自动刷新 UI |
Column / Row |
布局 | 垂直和水平排列子组件 |
List / ListItem |
列表 | 渲染列表结构 |
ForEach |
循环渲染 | 把数组数据变成 UI 组件 |
Image |
图标 | 显示本地图片资源 |
.onClick() |
事件 | 处理用户点击交互 |
if...else |
条件渲染 | 根据状态显示不同 UI |
.decoration() |
文本样式 | 添加删除线效果 |
一个小小的待办清单,其实把 ArkTS 声明式 UI 的核心用法都过了一遍。如果你能把这段代码完全搞明白,后面学更复杂的案例就不会那么吃力了。
更多推荐

所有评论(0)