【共创季稿事节】鸿蒙原生ArkTS动态列表布局实战_State_ForEach完整指南
鸿蒙原生 ArkTS 布局实战:@State + ForEach 动态列表布局完全指南
一、引言
1.1 背景
2024 年第四季度,HarmonyOS NEXT(鸿蒙星河版)正式面向开发者开放。这是华为彻底剥离 Android AOSP 代码、全栈自研的操作系统版本。从这一刻起,鸿蒙应用的开发语言栈也发生了根本性变化——ArkTS 取代了 JavaScript 和 Java,成为鸿蒙原生应用的一等公民。
ArkTS 是 TypeScript 的超集,它在 TypeScript 的基础上增加了声明式 UI 描述、状态管理、编译时校验等特性,语法上更接近 SwiftUI 和 Jetpack Compose。对于有过 Android 或 iOS 开发经验的读者来说,ArkTS 的学习曲线其实相当平缓。
1.2 为什么是 @State + ForEach?
在任何一个面向用户的应用程序中,列表渲染都是最核心、最高频的 UI 场景。
- 社交应用的信息流
- 电商应用的商品列表
- 即时通讯的会话列表
- 效率工具的任务清单
这些场景都有一个共同的特点:数据是动态的,UI 需要随着数据的变化实时响应。在鸿蒙 ArkTS 中,@State 装饰器和 ForEach 指令就是解决这个问题的标准组合。@State 负责"感知数据变化",ForEach 负责"高效渲染列表项",二者配合构成了鸿蒙原生动态列表布局的基石。
1.3 本文目标
本文将通过一个完整的任务管理 Demo 应用,逐行讲解 @State + ForEach + List 组合的使用方法、底层原理和最佳实践。无论你是刚接触 HarmonyOS 开发的新手,还是从其他平台迁移过来的开发者,读完本文后都应该能够:
- 理解
@State装饰器的响应式原理 - 掌握
ForEach的正确用法和 keyGenerator 的重要性 - 学会在
List容器中高效渲染动态数据 - 了解 API 24 中状态管理和列表渲染的新特性
- 避免常见的性能陷阱和语法错误
二、核心技术详解
2.1 @State 装饰器:响应式数据的基石
2.1.1 基本原理
@State 是 ArkTS 中最基本的状态装饰器,它的工作可以用一句话概括:
当 @State 修饰的变量发生变化时,所有依赖该变量的 UI 组件会自动重新渲染。
这个机制在底层是通过编译时代码变换实现的。ArkTS 编译器会在构建阶段扫描所有 @State 变量和它们被引用的 UI 代码,生成一个依赖追踪图。当变量的 setter 被触发时,框架沿着依赖图找到受影响的组件,只更新这些组件的 UI 节点——而不是整个页面重绘。
2.1.2 支持的数据类型
@State 可以修饰以下类型:
| 类型 | 示例 | 变更检测方式 |
|---|---|---|
| 基本类型 | string, number, boolean |
直接赋值触发 |
| 枚举 | enum Color { Red, Green } |
赋值触发 |
| 对象 | class TaskItem { ... } |
属性修改也触发(深度监听) |
| 数组 | TaskItem[] |
push/splice/pop 及元素属性修改均触发 |
其中最重要的是 对象和数组的深度监听。在 Jetpack Compose 中,修改 MutableList 中的元素属性需要手动触发重组;而在 ArkTS 中,item.completed = true 这样的操作会被框架自动捕获并触发 UI 刷新。这大大简化了代码编写。
2.1.3 使用限制
在 ArkTS 的严格模式下(API 24 默认开启),@State 有以下限制:
- 不能在非 @Component 结构体中使用:
@State只能在被@Component装饰的结构体中使用 - 初始化不能依赖其他 @State 变量:
@State变量的初始值必须在声明时确定,或者通过aboutToAppear生命周期初始化 - 不支持计算属性:如果需要一个由其他状态派生出的值,应该使用
@Prop或@Computed(API 24 新增)
2.2 ForEach:高效的循环渲染指令
2.2.1 基本语法
ForEach(
arr: any[], // 数据源数组
itemGenerator: (item, index) => void, // UI 生成器
keyGenerator?: (item, index) => string // Key 生成器(推荐必传)
)
三个参数各自的责任:
- 数据源数组:必须是
@State或其他状态装饰器修饰的响应式数组 - UI 生成器:为数组中的每一项返回一个 UI 组件树
- Key 生成器(可选但强烈推荐):为每一项返回一个唯一且稳定的字符串标识
2.2.2 keyGenerator 为什么至关重要
这是初学者最容易忽略、也最容易踩坑的地方。
当你不传 keyGenerator 时,ForEach 会默认使用数组下标(index)作为 key。这在很多情况下会导致错误的节点复用:
场景模拟:
初始数据:[A, B, C] → 渲染三个列表项(key: 0, 1, 2)
执行操作:在 A 前面插入 X
期望结果:[X, A, B, C] → 渲染四个列表项
实际结果(无 keyGenerator):
- key 0 绑定的节点从 A → X(内容突变)
- key 1 绑定的节点从 B → A
- key 2 绑定的节点从 C → B
- 新增 key 3 → C
这会导致:
- 列表项的动画异常(不该移动的项发生了位移)
- 状态丢失(比如某个列表项的
TextInput正在编辑中,key 变化后组件被销毁重建) - 不必要的性能开销(框架需要对比更多的节点差异)
正确做法是使用数据本身的唯一 ID 作为 key:
ForEach(
this.taskList,
(item: TaskItem) => { ListItem() { ... } },
(item: TaskItem): string => item.id.toString() // 稳定且唯一
)
2.2.3 与 JavaScript Array.map 的区别
很多初学者会问:为什么不能用 this.list.map() 来渲染列表?
| 对比维度 | ForEach | Array.map |
|---|---|---|
| 响应式更新 | ✅ 框架级 Diff,只更新变化的节点 | ❌ 每次重新执行生成全新节点 |
| 节点复用 | ✅ 通过 key 复用已有组件实例 | ❌ 每次都销毁重建 |
| 虚拟滚动 | ✅ 与 List 配合支持按需渲染 | ❌ 无法参与 List 的虚拟滚动机制 |
| 批量操作 | ✅ 增删移动都能精确更新 | ❌ 只能整体替换 |
简单来说,ForEach 是框架层面的声明式渲染指令,而 map 只是一个纯 JavaScript/TypeScript 的数组方法。在 ArkTS 中,动态列表必须使用 ForEach。
2.3 List 容器:高效的滚动列表
2.3.1 List 的核心优势
List 是鸿蒙提供的高性能可滚动列表容器,它的核心能力是虚拟滚动(Virtual Scrolling):
- 只渲染当前可视区域内的列表项
- 列表项滑出屏幕时,组件实例被回收
- 新的列表项滑入屏幕时,复用已回收的实例
这意味着即使数据源有 十万条数据,List 也只维护屏幕内可容纳的几十个组件实例,内存占用几乎不随数据量增长。
2.3.2 List 的核心属性
List() {
// ForEach 放在这里
}
.width('100%')
.height('100%')
.divider({ strokeWidth: 1, color: '#EEEEEE' }) // 分割线
.edgeEffect(EdgeEffect.Spring) // 边缘回弹
.sticky(StickyStyle.Header) // 粘性头部
.scrollBar(BarState.Auto) // 滚动条
在 API 24 中,List 新增了以下能力:
cachedCount:控制屏幕外缓存列表项的数量,优化快速滚动时的白屏问题scrollToIndex:支持动画平滑滚动到指定索引nestedScroll:支持嵌套滚动场景(如列表内的可滚动子组件)
2.3.3 ListItem 与布局
每个由 ForEach 生成的列表项必须用 ListItem 包裹:
List() {
ForEach(this.taskList, (item: TaskItem) => {
ListItem() {
// 这里放卡片布局
}
.swipeAction({ end: this.SwipeDeleteButton(item) })
}, (item) => item.id.toString())
}
ListItem 支持以下高级交互:
.swipeAction:滑动操作菜单(左滑/右滑显示按钮).sticky:将该列表项设为粘性头部.selectable:是否可选中(用于选择模式).onSelect:选中事件回调
三、Demo 应用完整解析
下面我们逐段分析示例应用中的核心代码。
3.1 数据模型定义
class TaskItem {
id: number;
title: string;
description: string;
completed: boolean;
priority: '高' | '中' | '低';
constructor(id: number, title: string, description: string, priority: '高' | '中' | '低') {
this.id = id;
this.title = title;
this.description = description;
this.completed = false;
this.priority = priority;
}
}
为什么用 class 而不是 interface?
因为 @State 需要可变的引用类型才能触发深度监听。interface 在 TypeScript 中只是编译时类型约束,运行时不存在。如果使用 interface,修改 item.completed = true 框架无法感知变化。
API 24 的最佳实践:在 API 24 中,推荐使用 @Observed 装饰器配合 @ObjectLink 来实现更精细的跨组件状态同步。
3.2 组件结构
@Entry
@Component
struct Index {
@State private taskList: TaskItem[] = [];
@State private inputTitle: string = '';
@State private selectedPriority: '高' | '中' | '低' = '中';
private nextId: number = 1;
// ...
}
代码要点:
@Entry:标记该组件为应用入口页面,对应main_pages路由表中的第一个页面@Component:声明这是一个自定义组件,支持装饰器语法@State放在 struct 顶层:所有状态变量都在结构体顶部声明,一目了然nextId没有使用@State:因为它只用于生成 ID,不直接影响 UI 渲染
3.3 数据初始化(aboutToAppear)
aboutToAppear(): void {
const samples: Array<[string, string, '高' | '中' | '低']> = [
['学习 @State 装饰器', '掌握声明式响应式数据绑定', '高'],
['理解 ForEach 用法', '学会 keyGenerator 和按需渲染', '高'],
['实践 List 组件', '熟悉 List + ForEach 组合模式', '中'],
['探索 Grid 布局', '了解网格布局的参数配置', '低'],
['完成第一篇笔记', '将学到的知识整理成文档', '中'],
];
samples.forEach((sample) => {
const title = sample[0];
const desc = sample[1];
const priority = sample[2];
this.taskList.push(new TaskItem(this.nextId++, title, desc, priority));
});
}
生命周期钩子 aboutToAppear:在组件即将显示时调用,是初始化数据的推荐位置。需要注意的是,ArkTS 不支持构造函数中初始化 UI 依赖的数据,因为 @State 变量在构造阶段尚未完成依赖追踪的注册。
3.4 核心布局:List + ForEach
build() {
Column() {
// ... 顶部 UI ...
List() {
ForEach(
this.taskList, // ① 数据源
(item: TaskItem, index?: number) => { // ② UI 生成器
ListItem() {
this.TaskCard(item, index ?? 0)
}
.swipeAction({ end: this.SwipeDeleteButton(item) })
},
(item: TaskItem): string => item.id.toString() // ③ Key 生成器
)
}
.width('100%')
.layoutWeight(1)
.divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 })
.edgeEffect(EdgeEffect.Spring)
// ... 底部 UI ...
}
}
布局层级关系:
Column
├── Row (标题栏)
├── Row (输入区域)
├── Row (操作按钮)
├── List ─────────────────── 虚拟滚动容器
│ ├── ListItem #0 ──── TaskCard(A)
│ ├── ListItem #1 ──── TaskCard(B)
│ ├── ListItem #2 ──── TaskCard(C)
│ └── ...
└── Text (底部提示)
layoutWeight(1) 是一个特别有用的属性——它让 List 在 Column 中占据所有剩余空间,确保列表高度自适应不同屏幕尺寸。
3.5 使用 @Builder 封装列表项 UI
@Builder
TaskCard(item: TaskItem, index: number) {
Row() {
Text(`${index + 1}`) // 序号
.fontSize(13).fontColor('#BBBBBB').width(28)
Column() { // 内容区
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Medium)
.fontColor(item.completed ? '#BBBBBB' : '#333333')
.decoration({
type: item.completed
? TextDecorationType.LineThrough
: TextDecorationType.None
})
if (item.description) {
Text(item.description)
.fontSize(12).fontColor('#999999').margin({ top: 2 })
}
}
.layoutWeight(1)
Text(item.priority) // 优先级标签
.backgroundColor(/* 高=红, 中=黄, 低=灰 */)
Text(item.completed ? '✅' : '⬜') // 状态图标
}
}
为什么用 @Builder 而不是独立的 @Component?
@Builder 是当前组件的内部构建函数,它可以直接访问父组件的 @State 变量和方法(如 toggleCompleted),不需要通过 @Prop 或 @Link 传递回调。对于简单的列表项 UI,使用 @Builder 更简洁。
如果列表项 UI 足够复杂,或者需要在多处复用,就应该提取为独立的 @Component 子组件,通过 @Prop 接收数据。
3.6 动态数据操作三部曲
应用演示了三种最常见的数据操作模式:
模式一:新增 — 数组 push
private addTask(): void {
this.taskList.push(new TaskItem(/* ... */));
// UI 自动新增一行
}
push 触发了数组的 length 变化和新增元素,框架在下一帧自动插入一个新的 ListItem。
模式二:删除 — 数组 splice
private removeTask(id: number): void {
const index = this.taskList.findIndex(item => item.id === id);
if (index !== -1) {
this.taskList.splice(index, 1);
// UI 自动删除对应行(带从右向左的默认移除动画)
}
}
splice 触发了数组元素移除,框架找到 key 对应的组件并执行移除动画。
模式三:更新 — 元素属性修改
private toggleCompleted(id: number): void {
const item = this.taskList.find(task => task.id === id);
if (item) {
item.completed = !item.completed;
// UI 自动刷新该行:文字颜色、删除线、图标全部更新
}
}
这是最令人惊艳的一点——直接修改对象属性就能触发 UI 刷新,无需任何额外的"通知"机制。这是因为 @State 在编译时为 TaskItem 类的属性注入了代理 setter,每次修改都会被捕获。
四、API 24 新特性与迁移指南
4.1 API 24 的新增能力
HarmonyOS NEXT API 24(对应 SDK 7.0.0)在状态管理和列表渲染方面引入了多项重要改进:
4.1.1 @Computed 装饰器
@State private tasks: TaskItem[] = [];
@Computed
get incompleteCount(): number {
return this.tasks.filter(t => !t.completed).length;
}
@Computed 类似于 Vue 的 computed 或 SwiftUI 中依赖 @State 的计算属性。它会自动追踪内部依赖的 @State 变量,只有依赖变化时才重新计算,避免不必要的重复计算。
4.1.2 @Observed 和 @ObjectLink
对于跨组件的嵌套对象状态同步,API 24 推荐使用 @Observed + @ObjectLink:
@Observed
class TaskItem {
// ...
}
@Component
struct TaskCard {
@ObjectLink item: TaskItem; // 共享同一个对象引用
// 修改 item 会同时反映到父组件
}
4.1.3 LazyForEach 正式稳定
在 API 24 中,LazyForEach 从实验性功能转为正式 API。与 ForEach 的区别:
| 特性 | ForEach | LazyForEach |
|---|---|---|
| 数据加载方式 | 一次性全部加载 | 按需加载(支持数据源分页) |
| 大数据量 | 受限于内存 | 支持百万级数据 |
| 数据源接口 | 普通数组 | 需实现 IDataSource 接口 |
| 适用场景 | 中小型列表(< 1000 项) | 大型列表(1000+ 项) |
4.1.4 List 的视觉刷新
animateItem:支持为列表项的增删移动指定自定义动画stickyHeader增强:支持多级粘性头部scrollBar可定制:自定义滚动条样式和宽度
4.2 从 API 23 迁移到 API 24
如果你的项目当前使用 API 23,迁移到 API 24 需要关注以下几点:
-
showToast完全移除:promptAction.showToast在 API 23 中已弃用,API 24 中彻底移除。替代方案是使用@ohos.prompt模块的showToast或showDialog。 -
严格模式默认开启:API 24 中
arkts-no-destruct-params、arkts-no-untyped-obj-literals等限制不再只是警告,而是编译错误。建议在开发阶段就开启这些规则。 -
ForEach keyGenerator 必传:在 API 24 中,不传
keyGenerator会触发编译警告,强烈建议始终提供稳定的 key 生成器。 -
@State深度监听行为调整:API 24 中对嵌套属性的监听更严格,如果修改了未在@State声明时出现的属性,可能不会被捕获。建议声明所有需要监听的属性。
五、项目实战:从零搭建动态任务管理应用
理论知识学完了,现在我们从一个空白页面开始,完整地走一遍开发流程。这不仅能巩固前面的知识点,还能帮助你建立鸿蒙应用开发的整体思维。
5.1 第一步:设计数据模型
任何数据驱动的应用,第一步都是设计数据模型。我们的任务管理应用需要记录以下信息:
- 唯一标识(id):用于 ForEach 的 key 生成,也用于查找和删除操作
- 标题(title):任务的简要描述,必填字段
- 详细描述(description):可选的补充说明
- 完成状态(completed):布尔值,控制 UI 上的贯穿样式和图标
- 优先级(priority):三档可选——高、中、低,影响标签颜色
选择 class 而非 interface 的理由我们已经讨论过——@State 需要对嵌套属性做代理监听,而 class 的实例是引用类型,便于框架注入响应式代理。
设计思考:为什么不在模型中包含创建时间、截止时间等字段?在设计初期,应该遵循"最小可行模型"原则——只包含当前 UI 明确需要的字段。后续可以通过继承或组合来扩展。过度设计反而会让初学者分不清主次。
5.2 第二步:组织组件结构
一个页面的组件层次结构直接影响可维护性。我们的应用采用"一页一组件"的简单结构:
Index(根页面)
├── 标题栏(Row)
├── 输入区域(Row)
│ ├── TextInput(标题输入框)
│ ├── Select(优先级选择器)
│ └── Button(添加按钮)
├── 操作按钮行(Row)
│ ├── Button(清除已完成)
│ └── Button(重置示例)
├── List(核心列表区域)
│ ├── ListItem × N(任务卡片)
│ └── 每个 ListItem 包含:
│ ├── 序号 Text
│ ├── 内容 Column(标题 + 描述)
│ ├── 优先级标签 Text
│ └── 状态图标 Text
└── 底部提示(Text)
这种分层清晰的结构有以下几个好处:
- 自上而下的阅读顺序:符合用户浏览页面的自然视线流向
- 每个区域职责单一:标题栏只做标题展示,输入区域只处理新增逻辑,互不干扰
- 便于拆分组件:当某个区域逻辑变复杂时(比如输入区域需要表单校验),可以轻松提取为独立组件
5.3 第三步:实现状态管理
在 ArkTS 中,状态管理的核心是"哪些数据会变化,变化后影响哪些 UI"。我们的应用中:
| 状态变量 | 变化时机 | 影响的 UI |
|---|---|---|
taskList |
增/删/改任务 | 整个列表区域 |
inputTitle |
用户输入 | 输入框的显示值 |
selectedPriority |
用户切换 | 选择器的显示值 |
一个常见的误区是"把所有变量都用 @State 装饰"。实际上,只有直接参与 UI 渲染或影响用户交互感知的数据才需要设为状态。比如 nextId 只用于生成 ID,不直接影响界面,所以就不需要 @State。
5.4 第四步:编写 UI 布局
UI 布局的编写遵循"从外到内、从大到小"的顺序:
- 确定容器:最外层用
Column实现垂直排列 - 划分区域:标题栏、输入区、按钮区、列表区、底部提示
- 填充内容:在每个区域内填入组件
- 调整细节:间距、颜色、字体、交互反馈
鸿蒙 ArkTS 的布局思维与传统前端(HTML + CSS)有显著区别:
- 没有"盒模型"的外边距合并问题,
padding和margin行为更接近 SwiftUI - 使用
.layoutWeight(1)替代 Flexbox 的flex: 1 - 组件通过链式调用设置属性,每个属性方法都返回组件本身
- 条件渲染使用
if语句而非三元表达式或v-if指令
5.5 第五步:添加交互逻辑
交互逻辑的添加需要遵循"数据驱动"而不是"事件驱动"的思维:
传统方式(事件驱动):
用户点击 → 事件回调 → 直接操作 DOM → 页面变化
鸿蒙方式(数据驱动):
用户点击 → 事件回调 → 修改 @State 数据 → 框架自动更新 UI
在我们的应用中,所有交互都遵循后者:
- 点击添加按钮 →
addTask()→this.taskList.push(...)→ 列表自动新增一行 - 点击完成图标 →
toggleCompleted()→item.completed = !item.completed→ 文字颜色和删除线自动变化 - 左滑删除 →
removeTask()→this.taskList.splice(...)→ 列表项自动移除带动画
这种思维转变是理解 ArkTS 的关键。开发者不再需要思考"如何操作 UI",只需要思考"数据应该变成什么样"。
5.6 第六步:调试与验证
在 DevEco Studio 中运行应用时,可以利用以下调试手段:
- Previewer 预览:代码编辑时实时查看布局效果,适合调整视觉细节
- Inspector 调试:运行时查看组件树和状态值,确认 @State 变量是否正确更新
- Profiler 性能分析:监测列表滑动帧率,验证虚拟滚动是否生效
- 日志输出:使用
console.info()在调试面板打印状态变化,追踪数据流
一个实用的调试技巧是在 aboutToAppear 中加入 console.info,确认生命周期钩子按预期执行:
aboutToAppear(): void {
console.info('[Index] aboutToAppear triggered, initializing data...');
// 初始化数据
}
六、实际场景扩展
6.1 对接网络接口
在实际业务中,数据通常来自后端 API。将我们的 Demo 改造为真实应用需要以下步骤:
- 引入网络请求模块:使用
@ohos.net.http或第三方库 - 添加加载状态:定义
@State isLoading: boolean = false控制加载动画 - 实现分页加载:结合
LazyForEach和IDataSource接口,实现上拉加载更多 - 错误处理:捕获网络异常,显示错误提示和重试按钮
private async loadTasksFromAPI(): Promise<void> {
this.isLoading = true;
try {
const response = await http.request('https://api.example.com/tasks');
const data = JSON.parse(response.result as string) as TaskItem[];
this.taskList = data;
} catch (error) {
console.error('Failed to load tasks:', error);
// 显示错误提示
} finally {
this.isLoading = false;
}
}
6.2 列表项编辑模式
除了展示和删除,很多场景需要支持原地编辑。实现思路如下:
- 定义
@State editingId: number | null = null标记当前正在编辑的项 - 在
TaskCard中根据editingId决定显示Text还是TextInput - 编辑完成后调用回调更新
taskList中对应的数据
// 在 ListItem 中
if (this.editingId === item.id) {
TextInput({ text: item.title })
.onChange((value) => { item.title = value; })
} else {
Text(item.title)
.onClick(() => { this.editingId = item.id; })
}
6.3 跨组件通信进阶
当应用规模增大,父子组件之间、兄弟组件之间的状态共享需求就会出现。API 24 提供了多种跨组件通信方案:
| 方式 | 装饰器 | 适用场景 |
|---|---|---|
| 父子传值 | @Prop |
父传子,单向数据流 |
| 父子共享 | @Link |
双向同步,父子共享同一引用 |
| 跨层共享 | @Provide + @Consume |
祖先传后代,跳过中间层级 |
| 全局共享 | AppStorage / LocalStorage | 跨页面、跨组件的全局状态 |
6.4 列表搜索与筛选
在真实应用中,列表数据通常需要搜索和筛选。实现方式是在 @State 数据源之上再加一层"计算"逻辑:
@State private searchKeyword: string = '';
// 在 build 中使用过滤后的数组
get filteredList(): TaskItem[] {
if (!this.searchKeyword.trim()) {
return this.taskList;
}
return this.taskList.filter(item =>
item.title.includes(this.searchKeyword)
);
}
在 API 24 中,可以将此逻辑封装为 @Computed 属性,让框架自动追踪依赖并缓存计算结果。
七、性能优化与最佳实践
本章节承接上文第六部分的实际场景扩展,深入探讨列表性能的优化方法和常见陷阱。
7.1 列表性能优化七条
7.1.1 始终提供稳定的 keyGenerator
这是最重要的一条优化。稳定的 key 让框架能够精确识别每个列表项,最小化 DOM 更新的范围。使用数据库或数据中台返回的唯一 ID,避免使用数组下标或随机数。
7.1.2 避免在 ForEach 内部创建匿名函数
❌ 不推荐:
ForEach(this.list, (item) => {
ListItem() {
Button('点击')
.onClick(() => this.doSomething(item)) // 每次渲染都创建新函数
}
})
✅ 推荐:
ForEach(this.list, (item) => {
ListItem() {
Button('点击')
.onClick(() => this.doSomething(item.id)) // 通过 ID 查找
}
})
或者使用 @Builder 提取列表项。
7.1.3 控制列表项的高度
给每个 ListItem 或内部的根容器设置明确的 height,可以帮助 List 更准确地计算滚动高度,避免滚动条跳动。
7.1.4 使用 cachedCount 优化快速滚动
List() { ... }
.cachedCount(10) // 在屏幕外额外缓存 10 个列表项
当用户快速滑动列表时,cachedCount 可以让新的列表项提前渲染好,减少白屏时间。
7.1.5 大数据量使用 LazyForEach
当数据量超过 1000 条时,切换到 LazyForEach + IDataSource。LazyForEach 支持数据分页、延迟加载和按需销毁,内存占用更可控。
7.1.6 避免在 ForEach 中嵌套 ForEach
嵌套的 ForEach 会导致 diff 算法的复杂度指数级上升。如果确实需要嵌套列表(如评论回复),考虑扁平化数据或使用单独的列表组件。
7.1.7 使用 shouldUpdate 控制刷新范围
在 API 24 中,可以通过 shouldUpdate 生命周期钩子手动控制组件是否需要重新渲染:
@Component
struct TaskCard {
@Prop item: TaskItem;
shouldUpdate(nextProps: Record<string, Object>): boolean {
return (nextProps['item'] as TaskItem).id !== this.item.id;
}
}
7.2 常见错误与解决方案
| 错误 | 症状 | 解决方案 |
|---|---|---|
| 未传 keyGenerator | 列表项动画异常、状态丢失 | 传入基于 ID 的 key 生成器 |
| 使用 interface 而非 class | 属性修改不触发 UI 刷新 | 改用 class 定义数据模型 |
| 在 ForEach 中直接修改数组 | UI 没有反应 | 使用 push/splice 或整体赋值 |
| 对象字面量作为 Builder 参数 | 编译失败:arkts-no-untyped-obj-literals |
使用独立参数或声明接口 |
| 在构造函数中初始化 @State | 运行时状态丢失 | 在 aboutToAppear 中初始化 |
| 解构函数参数 | 编译失败:arkts-no-destruct-params |
使用临时变量逐一赋值 |
八、完整代码清单
以下是完整的
Index.ets文件代码,可以直接在 HarmonyOS NEXT API 24 项目中运行。
/**
* 示例:鸿蒙原生 ArkTS 布局方式 —— @State + ForEach 动态列表布局
*
* ── 布局要点 ───────────────────────────────────────────────
* 1. @State 装饰器:声明式响应数据源,数据变化时自动触发 UI 刷新
* 2. ForEach 指令:基于数据源数组循环渲染列表项,keyGenerator 保证节点复用
* 3. List / Grid 容器:提供高效的可滚动列表/网格布局能力
* ───────────────────────────────────────────────────────────
*/
import { promptAction } from '@kit.ArkUI';
/**
* 数据模型 —— 列表中的每一项
*/
class TaskItem {
id: number;
title: string;
description: string;
completed: boolean;
priority: '高' | '中' | '低';
constructor(id: number, title: string, description: string, priority: '高' | '中' | '低') {
this.id = id;
this.title = title;
this.description = description;
this.completed = false;
this.priority = priority;
}
}
/**
* 主页面 —— 动态任务列表
*
* @Entry 标记该组件为应用入口页面
* @Component 标记为可复用的自定义组件
*/
@Entry
@Component
struct Index {
// ─── @State 响应式数据 ────────────────────────────────────
@State private taskList: TaskItem[] = [];
@State private inputTitle: string = '';
@State private selectedPriority: '高' | '中' | '低' = '中';
private nextId: number = 1;
/**
* 组件生命周期 —— aboutToAppear
* 在此处初始化模拟数据
*/
aboutToAppear(): void {
const samples: Array<[string, string, '高' | '中' | '低']> = [
['学习 @State 装饰器', '掌握声明式响应式数据绑定', '高'],
['理解 ForEach 用法', '学会 keyGenerator 和按需渲染', '高'],
['实践 List 组件', '熟悉 List + ForEach 组合模式', '中'],
['探索 Grid 布局', '了解网格布局的参数配置', '低'],
['完成第一篇笔记', '将学到的知识整理成文档', '中'],
];
samples.forEach((sample) => {
const title: string = sample[0];
const desc: string = sample[1];
const priority: '高' | '中' | '低' = sample[2];
this.taskList.push(new TaskItem(this.nextId++, title, desc, priority));
});
}
/**
* 添加任务
*/
private addTask(): void {
const title = this.inputTitle.trim();
if (!title) {
try { promptAction.showToast({ message: '请输入任务标题', duration: 1500 }); } catch (e) {}
return;
}
this.taskList.push(new TaskItem(this.nextId++, title, '', this.selectedPriority));
this.inputTitle = '';
try { promptAction.showToast({ message: `✅ 已添加:「${title}」`, duration: 1500 }); } catch (e) {}
}
/**
* 删除任务(按 ID)
*/
private removeTask(id: number): void {
const index = this.taskList.findIndex(item => item.id === id);
if (index !== -1) {
this.taskList.splice(index, 1);
try { promptAction.showToast({ message: '🗑️ 已删除', duration: 1000 }); } catch (e) {}
}
}
/**
* 切换任务完成状态
*/
private toggleCompleted(id: number): void {
const item = this.taskList.find(task => task.id === id);
if (item) {
item.completed = !item.completed;
}
}
/**
* 清空所有已完成的任务
*/
private clearCompleted(): void {
this.taskList = this.taskList.filter(item => !item.completed);
}
/**
* 重置为初始示例数据
*/
private resetToSample(): void {
this.taskList = [];
this.nextId = 1;
this.aboutToAppear();
}
// ─── UI 构建 ──────────────────────────────────────────────
build() {
Column() {
// ─── 顶部标题栏 ────────────────────────────────────
Row() {
Text('📋 动态任务列表')
.fontSize(22)
.fontWeight(FontWeight.Bold)
.textAlign(TextAlign.Start)
.layoutWeight(1)
Text(`共 ${this.taskList.length} 项`)
.fontSize(14)
.fontColor('#888888')
}
.width('100%')
.padding({ top: 16, bottom: 8, left: 16, right: 16 })
// ─── 输入区域:新增任务 ───────────────────────────────
Row() {
TextInput({ placeholder: '输入新任务标题…', text: this.inputTitle })
.onChange((value: string) => { this.inputTitle = value; })
.layoutWeight(1)
.height(40)
.backgroundColor('#F5F5F5')
.borderRadius(8)
.padding({ left: 12 })
Blank()
Select([{ value: '高' }, { value: '中' }, { value: '低' }])
.value(`优先: ${this.selectedPriority}`)
.onSelect((index: number) => {
const map: Array<'高' | '中' | '低'> = ['高', '中', '低'];
this.selectedPriority = map[index];
})
.height(40)
Blank()
Button('➕ 添加')
.onClick(() => this.addTask())
.height(40)
.fontSize(14)
.borderRadius(8)
}
.width('100%')
.padding({ left: 16, right: 16, top: 8, bottom: 8 })
.alignItems(VerticalAlign.Center)
// ─── 操作按钮行 ───────────────────────────────────
Row() {
Button('🧹 清除已完成')
.onClick(() => this.clearCompleted())
.fontSize(13).height(32).borderRadius(6)
.backgroundColor('#E74C3C')
Blank()
Button('🔄 重置示例')
.onClick(() => this.resetToSample())
.fontSize(13).height(32).borderRadius(6)
.backgroundColor('#2ECC71')
}
.width('100%')
.padding({ left: 16, right: 16, top: 4, bottom: 8 })
// ═══════════════════════════════════════════════════════
// 核心区域:@State + ForEach + List 动态列表布局
// ═══════════════════════════════════════════════════════
List() {
ForEach(
this.taskList,
(item: TaskItem, index?: number) => {
ListItem() {
this.TaskCard(item, index ?? 0)
}
.swipeAction({ end: this.SwipeDeleteButton(item) })
},
(item: TaskItem): string => item.id.toString(),
)
}
.width('100%')
.layoutWeight(1)
.divider({ strokeWidth: 1, color: '#EEEEEE', startMargin: 16, endMargin: 16 })
.edgeEffect(EdgeEffect.Spring)
// ─── 底部提示 ─────────────────────────────────────
Text('💡 左滑任务项可删除 | 点击任务项切换完成状态')
.fontSize(12).fontColor('#AAAAAA')
.textAlign(TextAlign.Center)
.width('100%').padding(12)
}
.width('100%').height('100%').backgroundColor('#FFFFFF')
}
// ─── @Builder 构建卡片和按钮 ───────────────────────────────
@Builder
TaskCard(item: TaskItem, index: number) {
Row() {
Text(`${index + 1}`).fontSize(13).fontColor('#BBBBBB').width(28).textAlign(TextAlign.Center)
Column() {
Text(item.title)
.fontSize(16).fontWeight(FontWeight.Medium)
.fontColor(item.completed ? '#BBBBBB' : '#333333')
.decoration({ type: item.completed ? TextDecorationType.LineThrough : TextDecorationType.None })
.width('100%')
if (item.description) {
Text(item.description).fontSize(12).fontColor('#999999').width('100%').margin({ top: 2 })
}
}
.layoutWeight(1).alignItems(HorizontalAlign.Start).justifyContent(FlexAlign.Center)
Text(item.priority)
.fontSize(11).fontColor('#FFFFFF')
.backgroundColor(item.priority === '高' ? '#E74C3C' : item.priority === '中' ? '#F39C12' : '#95A5A6')
.borderRadius(4).padding({ left: 6, right: 6, top: 2, bottom: 2 }).margin({ right: 8 })
Text(item.completed ? '✅' : '⬜').fontSize(18)
.onClick(() => this.toggleCompleted(item.id))
}
.width('100%').height(60).padding({ left: 8, right: 16 })
.alignItems(VerticalAlign.Center)
.onClick(() => this.toggleCompleted(item.id))
}
@Builder
SwipeDeleteButton(item: TaskItem) {
Button('🗑️ 删除')
.onClick(() => this.removeTask(item.id))
.height('100%').width(80).backgroundColor('#E74C3C').fontColor('#FFFFFF')
.fontSize(14).borderRadius(0)
}
}
九、总结与展望
9.1 核心要点回顾
-
@State是 ArkTS 响应式编程的基石。它将数据与 UI 绑定,让开发者只需关注数据的变化,框架自动处理 UI 的刷新。 -
ForEach必须配合稳定的 keyGenerator 使用。不传 key 或使用数组下标作为 key,会导致组件复用错误、动画异常和状态丢失。 -
List容器提供虚拟滚动机制。只渲染可视区域的列表项,支持百万级数据量的流畅滚动。 -
@Builder适合封装列表项 UI。对于简单场景,比提取独立@Component更简洁。 -
API 24 进一步强化了状态管理能力。
@Computed、@Observed、@ObjectLink、LazyForEach等特性让复杂场景的代码更加简洁高效。
9.2 下一步学习方向
- Grid 网格布局:
Grid+ForEach实现类似美团、淘宝的网格商品列表 - LazyForEach + IDataSource:对接网络分页接口,实现无限滚动加载
- @Provide + @Consume:跨多层组件的状态共享(类似 React Context)
- @Animatable:为列表项增删添加自定义动画
- 自定义组件复用:将
TaskCard提取为独立@Component,通过@Prop和@Link传递数据
9.3 写在最后
HarmonyOS NEXT 的 ArkTS 框架在设计上借鉴了 SwiftUI、Jetpack Compose 和 Flutter 等现代声明式 UI 框架的优点,同时结合了鸿蒙分布式、全场景的独特优势。@State + ForEach + List 的组合看似简单,却是构建几乎所有数据驱动页面(信息流、商品列表、聊天记录、待办事项等)的基础。
希望本文能帮助你快速上手鸿蒙原生开发。如果你在实际开发中遇到任何问题,欢迎在评论区留言交流。
版权声明:本文为 HarmonyOS NEXT 开发系列教程之一,遵循 CC BY-NC 4.0 许可协议。文中代码示例可自由用于个人学习和商业项目。
附录:文中涉及 API 24 特性的描述基于 HarmonyOS NEXT 7.0.0 Developer Preview,正式发布时如有调整请以官方文档为准。


更多推荐




所有评论(0)