从静态到动态:鸿蒙 ArkTS 列表组件与状态装饰器实战
前言
在上一篇入门内容中,我们已经掌握了 ArkTS 的基础页面结构、输入框与按钮等交互组件,实现了简单的用户输入交互。而这周,我完成了列表组件List与基础状态装饰器的学习,终于把之前的静态页面,升级成了可以动态渲染、支持数据更新的动态列表 ------ 这也是我们开发实际业务页面的核心一步。
很多新手在入门到这一步的时候,很容易搞混各种带@的装饰器,也会踩列表渲染的各种坑,所以这篇文章我把自己的学习笔记、踩过的坑都整理了出来,既能帮自己复盘加深理解,也希望能帮同样在入门鸿蒙开发的开发者少走弯路。
开发环境:DevEco Studio、API 10+
前置知识:已掌握 ArkTS 基础页面结构、
@State基础用法、输入框与按钮交互
一、先搞懂:基础状态装饰器到底是什么
在学列表之前,我们得先把之前刚学的状态装饰器 理清楚 ------ 这些带@的语法,是 ArkTS 实现响应式的核心,也是我们能做到 "数据改了 UI 自动更" 的关键。
入门阶段最常用的两个基础装饰器,其实很好区分:
| 装饰器 | 作用范围 | 核心特点 | 适用场景 |
|---|---|---|---|
@State |
组件内部私有 | 组件自己的状态,修改后自动触发 UI 刷新 | 管理组件自己的数据,比如列表数据、输入框内容 |
@Prop |
父子组件间 | 接收父组件传递的参数,单向绑定,子组件修改不影响父组件 | 自定义子组件,接收父组件传过来的展示数据 |
简单来说:
@State就是 "我自己的东西,我改了,我自己的 UI 跟着变"@Prop就是 "我爸给我的东西,我能看能用,但是我改了没用,我爸的东西不会变"
这就是 ArkTS 最基础的单向数据流:父组件管数据,子组件只管展示,子组件要改数据,得通知父组件来改,不能自己偷偷改 ------ 这也是新手最容易踩的第一个坑。
基础用法示例
typescript
// 子组件:展示单个用户信息
@Component
struct UserCard {
// 用@Prop接收父组件传过来的用户数据
@Prop userName: string;
@Prop age: number;
build() {
Column() {
Text("姓名:" + this.userName)
Text("年龄:" + this.age)
}
.padding(15)
.backgroundColor('#F5F5F5')
.borderRadius(8)
}
}
// 父组件
@Entry
@Component
struct ParentPage {
// 用@State管理自己的用户数据
@State user: {name: string, age: number} = {name: "张三", age: 20};
build() {
Column() {
// 把数据传给子组件
UserCard({ userName: this.user.name, age: this.user.age })
Button("修改年龄")
.onClick(() => {
// 父组件自己改@State的数据,UI自动刷新
this.user.age++;
})
}
.width('100%')
.justifyContent(FlexAlign.Center)
.padding(20)
}
}
二、列表组件 List:动态列表的核心
搞定了状态装饰器,我们再来学列表组件List------ 这是我们开发商品列表、通讯录、待办清单这类页面的核心,它可以承载大量的列表项,还自带滑动能力,不用我们自己写。
1. List 的基础结构
List是容器组件,里面的每一项都是ListItem,基础结构很简单:
typescript
List() {
ListItem() {
Text("列表项1")
}
ListItem() {
Text("列表项2")
}
}
但如果我们只有静态的几个项,那根本没必要用 List,List 的真正用处,是动态渲染数组数据 ,这就要用到循环渲染ForEach了。
2. 循环渲染 ForEach:把数组变成列表
ForEach是 ArkTS 提供的循环渲染 API,它可以遍历我们的数组,把每个数组元素变成对应的 UI 组件,这是动态列表的核心。
它有三个参数,我们必须记清楚:
- 要遍历的数组
- 遍历函数:把每个数组元素转成 UI 组件
- key 生成函数:给每个项生成唯一的 key,**这个千万不能忘!
typescript
// 模拟的列表数据
@State listData: string[] = ["第一项", "第二项", "第三项"];
build() {
List() {
// 遍历数组,生成列表项
ForEach(
this.listData,
(item: string) => {
ListItem() {
Text(item)
.padding(15)
}
},
// 生成唯一key,用数组的索引或者数据的唯一id
(item: string) => item
)
}
.width('100%')
}
这里一定要强调:key 必须是唯一的,不能重复!
很多新手刚学的时候,会忽略这个 key,结果就是当列表数据更新(比如删除、新增项)的时候,列表会出现渲染错乱,比如删除了第一项,第二项的状态跑到第三项去了 ------ 这就是因为没有 key,系统不知道哪个项对应哪个数据,只能按顺序复用,就乱掉了。
3. List 的常用配置
List 有几个非常常用的属性,我们做列表的时候基本都会用到:
space:列表项之间的间距divider:列表的分割线,用来区分不同的项flexGrow:让 List 占满剩余空间,这样内容超出的时候才会自动滑动
typescript
List() {
// ...列表项
}
.width('90%')
.flexGrow(1) // 占满剩余空间,实现滑动
.space(10) // 列表项之间的间距
.divider({ // 分割线配置
strokeWidth: 1, // 分割线宽度
color: '#EEEEEE', // 分割线颜色
startMargin: 15, // 分割线左边距
endMargin: 15 // 分割线右边距
})
三、综合实战:做一个动态待办列表
讲了这么多理论,我们来把所有知识点串起来,做一个完整的动态待办列表,这个小项目刚好能覆盖我们这周学的所有内容:
- 用
@State管理列表数据 - 用
@Prop实现父子组件传值 - 用
List+ForEach实现动态列表 - 实现待办的新增、删除功能
完整可运行代码
typescript
import { Column, List, ListItem, ForEach, Row, Text, TextInput, Button,
Color, FlexAlign, TextDecoration } from '@kit.ArkUI';
// 定义待办项的类型
interface Todo {
id: number;
content: string;
done: boolean;
}
// 子组件:单个待办项
@Component
struct TodoItem {
// 用@Prop接收父组件传过来的待办数据
@Prop todo: Todo;
// 接收父组件传过来的删除回调,用来通知父组件删除数据
private onDelete: () => void;
build() {
ListItem() {
Row() {
Text(this.todo.content)
.fontSize(16)
// 已完成的待办,文字变灰+删除线
.fontColor(this.todo.done ? '#999999' : '#333333')
.decoration(this.todo.done ? TextDecoration.LineThrough : TextDecoration.None)
// 占位组件,把删除按钮挤到最右边
Blank()
Button("删除")
.width(60)
.height(30)
.fontSize(12)
.backgroundColor(Color.Red)
.onClick(() => {
// 点击删除,调用父组件的回调,通知父组件更新数据
this.onDelete();
})
}
.width('100%')
.padding(15)
.backgroundColor(Color.White)
.borderRadius(8)
}
}
}
// 入口页面
@Entry
@Component
struct TodoListPage {
// 用@State管理整个待办列表的数据,父组件的私有状态
@State todoList: Todo[] = [
{ id: 1, content: '学习ArkTS基础语法', done: true },
{ id: 2, content: '掌握List列表组件', done: false },
{ id: 3, content: '搞懂状态装饰器区别', done: false },
];
// 输入框的内容
@State inputValue: string = "";
build() {
Column() {
// 页面标题
Text("我的待办列表")
.fontSize(24)
.fontWeight(FontWeight.Bold)
.margin({ top: 20, bottom: 20 })
// 新增待办的输入栏
Row() {
TextInput({ placeholder: '请输入待办内容', text: this.inputValue })
.flexGrow(1)
.height(45)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding({ left: 15 })
.onChange((value) => {
this.inputValue = value;
})
Button("添加")
.width(70)
.height(45)
.margin({ left: 10 })
.backgroundColor(Color.Blue)
.onClick(() => {
if (this.inputValue.trim() === "") return;
// 新增待办,修改@State数据,UI自动刷新
this.todoList.push({
id: Date.now(), // 用时间戳做唯一id
content: this.inputValue,
done: false
});
// 清空输入框
this.inputValue = "";
})
}
.width('90%')
.margin({ bottom: 15 })
// 待办列表
List() {
// 循环渲染所有待办项
ForEach(
this.todoList,
(todo: Todo) => {
TodoItem({
todo: todo,
// 给子组件传删除回调,父组件处理删除逻辑
onDelete: () => {
this.todoList = this.todoList.filter(item => item.id !== todo.id);
}
})
},
// 唯一key,用待办的id,保证不重复
(todo: Todo) => todo.id.toString()
)
}
.width('90%')
.flexGrow(1) // 让列表占满剩余空间,实现滑动
.space(10) // 列表项间距
.divider({ // 列表分割线
strokeWidth: 1,
color: '#EEEEEE',
startMargin: 15,
endMargin: 15
})
}
.width('100%')
.height('100%')
.backgroundColor('#F5F5F5')
.padding({ bottom: 20 })
}
}
效果说明
把这段代码复制到你的 DevEco Studio 里,直接就能运行,你可以得到一个完整的待办列表:
- 可以输入新的待办,点击添加,列表会自动新增项
- 可以点击删除,删掉对应的待办
- 列表内容多了之后,会自动支持滑动
- 已完成的待办会有灰色删除线的样式
四、学习踩坑:新手最容易犯的 5 个错误
这周学习的时候,我踩了不少坑,整理出来给大家避避:
- ForEach 忘记加 key:刚开始写列表的时候,我没加 key,结果删除待办的时候,剩下的项的状态全乱了,查了半天才发现是 key 的问题。
- 子组件直接改 @Prop :刚开始不懂单向数据流,想在子组件里直接改
this.todo.done,结果发现父组件的数据根本没变化,后来才知道要通知父组件来改。 - List 没加高度限制,导致不能滑动 :刚开始给 List 写了
wrapContent的高度,结果内容再多也不会滑动,后来才知道要给 List 设置固定高度或者flexGrow,让它有自己的滚动区域。 - 以为修改数组属性不会触发更新 :刚开始我以为要给
todoList重新赋值整个新数组才会刷新,后来发现直接push、修改对象属性,@State都能检测到,自动刷新 UI。 - key 用了数组索引:刚开始图省事,用数组的索引当 key,结果删除项的时候,索引变了,还是会出现渲染错乱,后来改成用数据自己的唯一 id 就好了。
五、学习总结
这周从 "写静态页面" 进入了 "写动态应用" 的阶段,核心掌握了这些内容:
- 两个基础状态装饰器的区别:
@State管组件内部状态,@Prop管父子传值 - 单向数据流的核心逻辑:父组件管数据,子组件只管展示,改数据要通知父组件
- 列表组件
List的基础用法,以及ForEach循环渲染的正确姿势,尤其是 key 的重要性 - 把所有知识点串起来,开发一个完整的动态列表应用
学完这些,其实我们已经能开发大部分的基础业务页面了,比如商品列表、用户列表、待办清单这些,都是基于这些知识点。