第七章:列表与渲染控制
本章配套代码:
code/Chapter7(ForEach + 条件渲染 Demo + 二级联动实战 + 仿抖音滑动案例) 前置要求:已完成状态管理与路由章节
7.1 学习目标
- 掌握
ForEach列表渲染与 key 生成函数 - 掌握
Repeat状态化列表渲染与对比选型 - 掌握
LazyForEach数据懒加载(大数据量列表) - 掌握
if/else条件渲染 - 理解数组更新触发刷新的正确方式
- 了解 List 懒加载与分页加载
- 掌握
List+ListItemGroup实现二级联动(实战案例) - 掌握
PanGesture实现上下滑动切换(仿抖音实战案例)
7.2 ForEach 列表渲染
7.2.1 基本用法
ForEach 遍历数组生成多个 UI 组件:
@Entry
@ComponentV2
struct ForEachExample {
@Local items: string[] = ['床前明月光', '疑是地上霜', '举头望明月']
build() {
Column() {
ForEach(this.items, (item: string, index: number) => {
Text(`${index + 1}. ${item}`)
.fontSize(16)
.padding(8)
})
}
}
}
语法:
ForEach(
数组, // ① 数据源
(item: T, index: number) => { }, // ② 生成 UI 的构建函数
(item: T) => string // ③ key 生成函数(可选)
)
7.2.2 key 生成函数
对象数组务必提供 key 生成函数,用于高效识别列表项:
interface Product {
id: number
name: string
price: number
}
@Entry
@ComponentV2
struct ProductList {
@Local products: Product[] = [
{ id: 1, name: '手机', price: 3999 },
{ id: 2, name: '耳机', price: 299 }
]
build() {
List() {
ForEach(
this.products,
(item: Product) => {
ListItem() {
Text(`${item.name}: ¥${item.price}`)
.padding(12)
}
},
(item: Product) => item.id.toString() // 用唯一 id 作 key
)
}
}
}
🔑 key 的作用:帮助框架最小化 DOM 更新。key 唯一稳定(用 id 而非 index),列表增删时性能最佳。
| key 选择 | 推荐度 | 说明 |
|---|---|---|
| 唯一 id | ✅ 推荐 | 如 item.id |
| index | ⚠️ 避免 | 增删时 key 错位,复用混乱 |
| 拼接字段 | 视情况 | 需保证唯一 |
7.2.3 数组更新方式(重要)
V2 中数组更新必须重新赋值才触发刷新:
// ✅ 正确:重新赋值
this.items = [...this.items, newItem] // 添加
this.items = this.items.filter((_, i) => i !== idx) // 删除
this.items = this.items.map(p => p.id === id ? { ...p, price: p.price + 100 } : p) // 修改
// ❌ 错误:原地修改不触发 UI 刷新
this.items.push(newItem)
this.items.splice(idx, 1)
7.3 条件渲染
7.3.1 if / else
@Entry
@ComponentV2
struct ConditionalExample {
@Local isLoggedIn: boolean = false
@Local userName: string = '张三'
build() {
Column({ space: 16 }) {
if (this.isLoggedIn) {
Text(`欢迎, ${this.userName}`)
.fontSize(20)
Button('退出')
.onClick(() => { this.isLoggedIn = false })
} else {
Text('请登录')
.fontSize(20)
Button('登录')
.onClick(() => { this.isLoggedIn = true })
}
}
.padding(16)
}
}
7.3.2 多条件分支
if (this.userRole === 'admin') {
Text('管理员面板').fontColor(Color.Red)
} else if (this.userRole === 'user') {
Text('用户面板').fontColor(Color.Blue)
} else {
Text('访客面板').fontColor(Color.Gray)
}
7.4 List 懒加载
7.4.1 List 基础
List + ListItem 实现滚动列表,仅渲染可见项(懒加载),适合大量数据:
@Entry
@ComponentV2
struct ListExample {
@Local items: string[] = ['床前明月光', '疑是地上霜', '举头望明月', '低头思故乡', '海内存知己', '天涯若比邻', '欲穷千里目', '更上一层楼', '春眠不觉晓', '处处闻啼鸟'] // ① 数据源
build() {
List({ space: 8 }) { // space:列表项间距
ForEach(this.items, (item: string) => {
ListItem() { // 每个列表项
Text(item)
.height(60)
}
})
}
}
}
| 组件 | 作用 |
|---|---|
List | 滚动容器(支持垂直/水平) |
ListItem | 单个列表项 |
ForEach | 遍历数据生成列表项 |
7.4.2 分页加载(onReachEnd)
滚动到底部自动加载更多:
@Entry
@ComponentV2
struct PaginationExample {
@Local items: number[] = []
@Local currentPage: number = 1
@Local isLoading: boolean = false
@Local hasMore: boolean = true
aboutToAppear(): void {
this.loadData()
}
async loadData() {
if (this.isLoading || !this.hasMore) return
this.isLoading = true
await new Promise(resolve => setTimeout(resolve, 1000)) // 模拟请求
const newItems = Array.from(
{ length: 20 },
(_, i) => (this.currentPage - 1) * 20 + i + 1
)
this.items = [...this.items, ...newItems] // 重新赋值追加
this.currentPage++
this.hasMore = this.currentPage <= 5
this.isLoading = false
}
build() {
List() {
ForEach(this.items, (item: number) => {
ListItem() {
Text(`Item ${item}`)
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
}
})
if (this.isLoading) {
ListItem() {
LoadingProgress()
.width(40).height(40)
}
}
}
.onReachEnd(() => { // 滚动到底触发
this.loadData()
})
}
}
💡
onReachEnd是 List 触底回调,是无限滚动/分页的标配。
7.4.3 实战案例:二级联动(List + ListItemGroup)
本案例来自华为官方 Codelabs《基于List组件实现二级联动效果》,演示左侧导航与右侧内容的二级联动:
- 切换左侧导航 → 右侧滚动到对应内容分组;
- 滚动右侧内容 → 左侧切换对应导航的选中态。
相关概念:
| 组件 / API | 作用 |
|---|---|
List | 列表容器,scroller 参数绑定 Scroller 对象实现编程式滚动 |
ListItemGroup | 列表分组,header 设置分组头(配合 sticky 实现吸顶) |
scrollToIndex() | 滚动到指定索引的列表项 |
onScrollIndex() | 监听当前滚动到的首项索引 |
StickyStyle.Header | 分组头吸顶,滚动时分组标题固定 |
代码结构:
entry/src/main/ets
├── common/constants/Constants.ets // 常量类
├── pages/IndexPage.ets // 二级联动页面入口
├── view
│ ├── ClassityItem.ets // 左侧导航项组件
│ └── CourseItem.ets // 右侧课程卡片组件
└── viewmodel
├── ClassifyModel.ets // 导航分类 Model
├── ClassifyViewModel.ets // 导航数据 ViewModel
├── CourseModel.ets // 课程 Model
└── LinkDataModel.ets // 原始数据 Model
联动核心逻辑(IndexPage.ets):
// 联动入口:isClassify 区分「点导航」还是「滚内容」
classifyChangeAction(index: number, isClassify: boolean): void {
if (this.currentClassify !== index) {
this.currentClassify = index
if (isClassify) {
this.scroller.scrollToIndex(index) // 点导航 → 滚动右侧内容
} else {
this.classifyScroller.scrollToIndex(index) // 滚内容 → 滚动左侧导航
}
}
}
build() {
Row() {
// 左侧导航 List(固定宽度)
List({ scroller: this.classifyScroller }) {
ForEach(this.classifyList, (item: ClassifyModel, index?: number) => {
ListItem() {
ClassifyItem({
classifyName: item.classifyName,
isSelected: this.currentClassify === index,
onClickAction: () => {
if (index !== undefined) {
this.classifyChangeAction(index, true)
}
}
})
}
}, (item: ClassifyModel) => item.classifyName.toString() + this.currentClassify)
}
.width($r('app.float.classify_item_width'))
.backgroundColor($r('app.color.classify_background'))
.scrollBar(BarState.Off)
// 右侧内容 List(ListItemGroup 分组 + 吸顶 header)
List({ scroller: this.scroller }) {
ForEach(this.classifyList, (classifyItem: ClassifyModel) => {
ListItemGroup({
header: this.ClassifyHeader(classifyItem.classifyName),
space: Constants.COURSE_ITEM_PADDING
}) {
ForEach(classifyItem.courseList, (courseItem: CourseModel) => {
ListItem() {
CourseItem({ itemStr: JSON.stringify(courseItem) })
}
}, (courseItem: CourseModel) => `${courseItem.courseId}`)
}
}, (item: ClassifyModel) => `${item.classifyId}`)
}
.sticky(StickyStyle.Header) // 分组头吸顶
.onScrollIndex((start: number) => this.classifyChangeAction(start, false))
.layoutWeight(1)
.edgeEffect(EdgeEffect.None)
}
}
联动原理:
- 点导航:点击左侧导航项 →
classifyChangeAction(index, true)→scroller.scrollToIndex(index)让右侧滚动到对应分组,并更新导航选中态。 - 滚内容:滚动右侧列表 →
onScrollIndex(start)回调当前首项所在分组索引 →classifyChangeAction(start, false)更新左侧选中态并滚动左侧导航。
运行效果:


💡 二级联动是
List+ListItemGroup+Scroller编程式滚动的经典应用,常用于电商分类、课程目录、通讯录等场景。
7.5 Repeat 列表渲染
7.5.1 基本用法
Repeat 是 V2 推荐的列表渲染方案,按 key 复用已有子组件(组件级渲染),数据频繁变化时性能优于 ForEach:
@Entry
@ComponentV2
struct RepeatExample {
@Local items: string[] = ['春眠不觉晓', '处处闻啼鸟', '夜来风雨声', '花落知多少'] // ① 数据源
build() {
List() {
Repeat(this.items, (item: string) => item) { // ② key 生成函数(第二参数)
(item: string, index: number) => { // ③ 构 建函数
ListItem() {
Text(`${index}: ${item}`)
.width('100%')
.height(60)
.textAlign(TextAlign.Center)
}
}
}
}
}
}
语法:
Repeat(
数组, // ① 数据源
(item, index) => string, // ② key 生成函数(可选,默认按索引)
template? // ③ 虚拟滚动模板(实验特性,暂不用)
) {
(item: T, index: number) => { } // ④ 生成 UI 的构建函数
}
7.5.2 ForEach vs Repeat
| 维度 | ForEach | Repeat |
|---|---|---|
| 渲染方式 | 函数式:按 key 重建子组件 | 组件式:key 级复用已有子组件 |
| 子组件内部状态 | 无状态,重建即丢失 | 有状态,可保留内部状态 |
| key 位置 | 第三参数 | 第二参数 |
| 性能 | 中小数据量够用 | 频繁增删改时更优 |
| 官方建议 | 简单静态列表 | 数据动态变化、需状态保持的列表 |
🔑 选型口诀:静态列表用
ForEach;动态增删改、需要组件状态复用用Repeat。