鸿蒙 ArkTS 实战进阶:组件复用三剑客与状态管理一篇通
Styles专门用来封装通用的、所有组件都能用的样式属性,比如宽高、边距、背景色这些,解决重复写样式的问题。// 全局@Styles:不用加this,所有组件都能用// 使用:直接调用,一行搞定所有通用样式Column() {Text("卡片内容").commonCard() // 直接复用样式@Styles只能写通用属性,不能写组件专属属性,比如 Text 的fontSize就不能写在里面。Fo
鸿蒙 ArkTS 实战进阶:组件复用三剑客与状态管理一篇通
前言
在上一篇入门文章里,我们搞定了 ArkTS 的基础页面结构、Text/TextInput/Button 这些基础组件,实现了最简单的用户交互 —— 这时候我们已经能写出 “能跑” 的页面了,但离 “写得好、写得规范、写得高效” 还有一段距离。
这周我把 ArkTS 开发的核心进阶知识点全部啃完了:从动态列表渲染,到 @State/@Prop/@Link 这套状态管理体系,再到 @Styles/@Extend/@Builder 这三个组件复用神器,还有 Swiper 轮播、Badge 角标这些高频实用组件,以及 Flex/Stack 布局进阶。
从 “把页面写出来” 到 “把页面写得优雅、不写重复代码”,这一周的学习可以说是 ArkTS 入门的第一个分水岭。这篇文章我把自己的学习笔记、踩过的坑全部整理出来,既是自己的复盘加深,也希望能帮同样在入门的开发者少走弯路。
开发环境:DevEco Studio、API 10+
前置知识:已掌握 ArkTS 基础页面结构、基础组件与简单交互
一、动态列表:从写死的页面到数据驱动的列表
在上一篇里,我们的页面内容都是写死的,而实际开发里 90% 的页面都是列表:商品列表、消息列表、待办列表…… 这就是我们要学的第一个核心:List组件 + ForEach循环渲染。
1. List 和 Column 的本质区别
很多新手一开始会问:我用 Column 也能堆出列表,为什么要用 List?
答案很简单:List 自带组件复用和滚动性能优化。
- Column 会把所有子组件一次性全部渲染,超过 10 个项之后就会开始卡顿,而且没有自带滚动
- List 只会渲染当前屏幕可见的项,滚动时自动复用组件,就算 1000 个项也能流畅滑动,自带滚动能力
2. ForEach 循环渲染:新手的第一个天坑
ForEach是 ArkTS 提供的响应式循环 API,它的作用就是把我们的数组数据,自动变成对应的 UI 组件,数据变了列表自动更新。它的语法很简单,但有一个 90% 的新手都会踩的坑:key 的设置。
// 错误写法:用数组索引当key,删除/新增项时会渲染错乱
ForEach(this.todoList, (item, index) => {
ListItem() {
Text(item.content)
}
}, (item, index) => index)
// 正确写法:用数据本身的唯一标识当key
ForEach(this.todoList, (item) => {
ListItem() {
Text(item.content)
}
}, (item) => item.id.toString())
为什么不能用索引当 key?
因为 key 是框架识别 “这个组件是谁” 的唯一标记,如果你删除了第一个项,原来第二个项的索引就变成了 0,框架就会误以为 “这个组件没变,只是内容变了”,直接复用原来的组件,就会出现状态错乱、输入框内容错位的问题。
3. List 常用配置
List() {
// ...列表项
}
.width('90%')
.flexGrow(1) // 核心:让List占满剩余空间,内容超出自动滚动
.space(10) // 列表项之间的间距
.divider({ // 列表分割线
strokeWidth: 1,
color: '#EEEEEE',
startMargin: 15,
endMargin: 15
})
这里再提一个新手坑:如果不给 List 设置固定高度或者flexGrow(1),它的高度会跟着内容自适应,内容再多也不会滚动 —— 我当时卡了一下午才发现这个问题。
二、状态管理:彻底搞懂三个带 @的核心装饰器
搞懂列表之后,最核心的就是 ArkTS 的状态管理体系 —— 这也是声明式 UI 的灵魂:数据变了,UI 自动刷新,不用我们手动改页面。
这周我学了三个最核心的状态装饰器,我用最通俗的方式给你讲明白:
| 装饰器 | 作用 | 数据流 | 初始化要求 | 适用场景 |
|---|---|---|---|---|
@State |
组件内部私有状态 | 组件内双向:改数据→UI 更新,UI 更新→数据变 | 必须本地初始化赋值 | 组件自己的数据:列表数据、输入框内容 |
@Prop |
父子组件单向传值 | 父→子单向:父组件数据变,子组件自动更;子组件改数据不影响父 | 可本地初始化,也可接收父组件传值 | 子组件接收父组件的展示数据 |
@Link |
父子组件双向同步 | 父子双向:父变子也变,子变父也变 | 必须从父组件接收状态引用,不能本地初始化 | 表单输入、滑块开关,需要双向联动的场景 |
用生活化的比喻一下就懂:
@State:你自己的笔记本,你随便写随便改,只有你自己能用@Prop:你朋友把他的笔记复印了一份给你,你在复印件上改,朋友的原件不会变@Link:你和朋友共用一个在线文档,你改了他那边也变,他改了你这边也变
基础示例:父子组件传值
// 子组件:待办项
@Component
struct TodoItem {
// 单向接收父组件传的内容
@Prop content: string;
// 双向绑定完成状态:子组件点勾选,父组件的状态也同步变
@Link isDone: boolean;
build() {
ListItem() {
Row() {
Text(this.content)
.decoration(this.isDone ? TextDecoration.LineThrough : TextDecoration.None)
Checkbox({ name: 'todo' })
.select(this.isDone)
.onChange((value) => {
// 子组件改@Link,父组件的状态自动同步
this.isDone = value;
})
}
}
}
}
// 父组件
@Entry
@Component
struct TodoList {
@State todoList: {id: number, content: string, done: boolean}[] = [
{id: 1, content: '学List组件', done: true},
{id: 2, content: '搞懂状态管理', done: false}
];
build() {
List() {
ForEach(this.todoList, (item) => {
TodoItem({
content: item.content,
// 用$符号传引用给@Link
isDone: $item.done
})
}, item => item.id.toString())
}
}
}
这里注意:给@Link传值的时候,要加$符号,代表传的是状态的引用,而不是值本身 —— 这也是新手容易忘的点。
三、组件复用三剑客:告别重复代码,写优雅的 ArkTS
这是我这周收获最大的部分:之前写页面,同样的按钮样式、同样的文本样式,每个组件都要写一遍重复的代码,而这三个装饰器,就是专门解决代码复用的问题,让我们的代码干净又好维护。
1. @Styles:定义通用样式集合
@Styles专门用来封装通用的、所有组件都能用的样式属性,比如宽高、边距、背景色这些,解决重复写样式的问题。
// 全局@Styles:不用加this,所有组件都能用
@Styles function commonCard() {
.width('90%')
.padding(15)
.backgroundColor(Color.White)
.borderRadius(8)
.shadow({ radius: 5, color: '#1A000000' })
}
// 使用:直接调用,一行搞定所有通用样式
Column() {
Text("卡片内容")
}
.commonCard() // 直接复用样式
注意:@Styles只能写通用属性,不能写组件专属属性,比如 Text 的fontSize就不能写在里面。
2. @Extend:扩展原生组件
@Extend专门用来扩展系统原生组件,比如给 Text、Button、Image 批量加自定义样式、事件,生成 “增强版” 的原生组件。
// 扩展Button:定义一个全局的主按钮样式
@Extend(Button) function mainButton() {
.width('90%')
.height(50)
.backgroundColor(Color.Blue)
.borderRadius(25)
.fontSize(16)
}
// 使用:所有主按钮直接用,不用重复写样式
Button("确认提交")
.mainButton()
Button("下一步")
.mainButton()
.margin({ top: 10 })
和@Styles的区别:@Extend是针对某个特定组件的扩展,可以写这个组件的专属属性,还能绑定事件;@Styles是通用的,所有组件都能用。
3. @Builder:封装可复用的 UI 片段
@Builder是三个里面功能最强的,它可以封装完整的 UI 结构,包括多个子组件、布局、逻辑,是最灵活的复用方式。
// 封装一个空状态的UI片段
@Builder function EmptyState(icon: Resource, tip: string) {
Column() {
Image(icon)
.width(80)
.margin({ bottom: 10 })
Text(tip)
.fontSize(14)
.fontColor('#999999')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
// 使用:列表为空的时候直接调用
if (this.todoList.length === 0) {
this.EmptyState($r('app.media.icon_empty'), "暂无待办事项")
}
这三个装饰器用熟了之后,你的代码里再也不会有大段重复的样式和 UI 片段,维护起来太舒服了。
四、高频实用组件与布局进阶
1. Swiper 轮播组件
Swiper 是做首页轮播图、引导页、Tab 切换的核心组件,自带触摸滑动、循环、自动播放所有功能,不用自己手写:
Swiper() {
Image($r('app.media.banner1'))
.objectFit(ImageFit.Cover)
Image($r('app.media.banner2'))
.objectFit(ImageFit.Cover)
Image($r('app.media.banner3'))
.objectFit(ImageFit.Cover)
}
.width('100%')
.height(180)
.loop(true) // 开启无限循环
.autoPlay(true) // 开启自动播放
.interval(3000) // 3秒切换一次
.indicator(true) // 显示底部指示器
2. Badge 角标组件
Badge 就是我们常见的消息红点、未读数字角标,用来做消息提示、标签标记:
// 数字角标:未读消息
Badge({ count: 5, maxCount: 99 }) {
Image($r('app.media.icon_message'))
.width(24)
}
.position({ x: 15, y: -5 })
// 红点角标:无数字提示
Badge({ position: BadgePosition.RightTop }) {
Image($r('app.media.icon_notify'))
.width(24)
}
3. 布局进阶
除了基础的 Column/Row,这周还学了两个核心布局:
- Flex 弹性布局:通过
flexDirection控制排列方向,justifyContent主轴对齐,alignItems交叉轴对齐,flexWrap控制换行,是复杂布局的核心 - Stack 堆叠布局:组件一层叠一层,通过
zIndex控制层级,alignContent控制对齐,用来做悬浮按钮、遮罩层、定位元素非常方便
五、学习踩坑总结:这些坑我帮你踩过了
- ForEach 用索引当 key,列表渲染错乱:这个是新手必踩坑,一定要用数据本身的唯一 id 当 key,不要图省事用索引
- List 没设置高度,无法滚动:一定要给 List 加
flexGrow(1)或者固定高度,不然内容超出也不会滚动 - @Link 传值忘记加(:给@Link传状态引用的时候,必须加
\)符号,不然传的是值,不是引用,不会双向同步 @Styles 里写组件专属属性,报错:@Styles 只能写通用属性,组件专属属性要写在 @Extend 里 - 子组件直接改 @Prop 数据,父组件不同步:记住单向数据流,@Prop 是父传子单向,子组件要改数据要通过事件通知父组件,或者用 @Link
六、学习总结
学完这周的内容,我最大的感受就是:终于从 “能写出页面”,变成了 “能写出规范、高效、可维护的页面”。
我们来复盘一下这周的核心收获:
- 掌握了动态列表的开发:List + ForEach,搞定所有列表类页面
- 搞懂了状态管理的核心:@State/@Prop/@Link,彻底理解声明式 UI 的数据驱动逻辑
- 学会了组件复用:@Styles/@Extend/@Builder,告别重复代码,写优雅的代码
- 掌握了高频组件与进阶布局:Swiper、Badge、Flex、Stack,能应对大部分业务场景
更多推荐



所有评论(0)