当前位置:首页>鸿蒙APP>《从零到一:鸿蒙原生应用开发实战》第13篇:List 组件详解——ForEach 循环渲染与 LazyForEach 懒加载

《从零到一:鸿蒙原生应用开发实战》第13篇:List 组件详解——ForEach 循环渲染与 LazyForEach 懒加载

  • 2026-10-29 17:59:33
《从零到一:鸿蒙原生应用开发实战》第13篇:List 组件详解——ForEach 循环渲染与 LazyForEach 懒加载

你好,我是华鸿技术栈的作者。

前几篇把鸿蒙开发的基础组件和 Flex 布局搞定了。Text、Image、Button、TextInput 会用,Column 和 Row 能把组件排好布局。今天开始搞列表。

列表是 鸿蒙开发里最常见的页面结构。新闻列表、商品列表、聊天记录、设置菜单——都是列表。鸿蒙里做列表核心就三个东西:List 容器、ListItem 子项、ForEach/LazyForEach 循环渲染。

这篇会讲静态列表的创建,动态数据的 ForEach 循环渲染,数据量大怎么用 LazyForEach 懒加载提升性能。附带一个文章列表的完整代码,后面做 App 的时候可以直接拿过去用。

13.1 静态列表:List + ListItem

先看静态列表——列表里每一项都是确定的数据,几条数据就几个 ListItem。

List() {  ListItem() {    Text('第一条')      .fontSize(16)      .width('100%')      .padding(16)  }  ListItem() {    Text('第二条')      .fontSize(16)      .width('100%')      .padding(16)  }  ListItem() {    Text('第三条')      .fontSize(16)      .width('100%')      .padding(16)  }}

List 是容器,ListItem 是每一项。List 默认就是滚动的,内容超出屏幕自动出滚动条。

分割线:每条之间加分割线,ListItem 里塞一个 Divider:

ListItem() {  Column() {    Text('第一条')      .fontSize(16)      .padding(16)    Divider()  }}

滚动方向:List 默认竖直滚动。水平滚动加 listDirection(Axis.Horizontal):

List() {  // ...}.listDirection(Axis.Horizontal)

水平列表在 Banner 轮播、横向标签栏里用到,大部分场景还是默认的竖直滚动。

13.2 循环渲染:ForEach

几条数据可以手写 ListItem,多了就用循环渲染:提供数据数组和每一项的 UI 模板,框架自动帮你生成列表。

@Entry@Componentstruct ArticleList {  @State articles: string[] = [    '鸿蒙开发入门',    'ArkTS 语法速成',    '声明式 UI 详解',    '@State 状态管理',    '@Prop 和 @Link',    'Flex 弹性布局'  ];  build() {    List() {      ForEach(this.articles, (item: string, index: number) => {        ListItem() {          Text(item)            .fontSize(16)            .padding(16)            .width('100%')        }      })    }  }}

ForEach 三个参数:数据数组、每一项的 UI 模板(回调函数,接收 item 和 index)、key 生成函数(可选,默认用 index)。框架自动遍历数组,每个元素生成一个 ListItem。

数据源变了,比如往 this.articles 里 push 一条新数据,列表自动刷新。这就是之前学的 @State 在起作用——数据变,UI 跟着变。

数据量不大,几十条、一两百条,用 ForEach 没问题。它一次性把所有 ListItem 都创建出来,数据少的时候渲染很快。

13.3 懒加载:LazyForEach

数据量大了,几百上千条,ForEach 一次性全创建会卡。LazyForEach 只创建屏幕上能看见的那几项,滑到哪加载哪,看不见的回收掉。

LazyForEach 和 ForEach 的写法差别,在于 LazyForEach 需要自己实现一个数据源类,实现 IDataSource 接口。看着复杂,其实就是一个固定套路,写一次以后到处复制。

// 数据源类:实现 IDataSourceclass ArticleDataSource implements IDataSource {  private articles: string[] = [];  constructor(articles: string[]) {    this.articles = articles;  }  totalCount(): number {    return this.articles.length;  }  getData(index: number): string {    return this.articles[index];  }  registerDataChangeListener(): void {}  unregisterDataChangeListener(): void {}}@Entry@Componentstruct LazyArticleList {  @State articles: ArticleDataSource = new ArticleDataSource([    '鸿蒙开发入门', 'ArkTS 语法速成', '声明式 UI 详解',    '@State 状态管理', '@Prop 和 @Link', 'Flex 弹性布局',    // 实际项目里这里可能有几百条数据  ]);  build() {    List() {      LazyForEach(this.articles, (item: string, index: number) => {        ListItem() {          Text(item)            .fontSize(16)            .padding(16)            .width('100%')        }      })    }  }}

ArticleDataSource 实现 IDataSource 接口,最少要写四个方法:totalCount(返回数据总数)、getData(返回第几项的数据)、registerDataChangeListener 和 unregisterDataChangeListener(数据变化监听,暂时空着)。

写一次,以后做列表页面直接复制这个类,换个数据类型就行。

数据几十条以内,ForEach 省事。数据上百条,或者不确定未来会不会加很多数据,直接用 LazyForEach。

几个注意点:

  • LazyForEach 的 key 生成器(默认用 index)要保证每条数据有唯一标识,用 index 在数据顺序不变的情况下没问题。如果列表会增删改,key 最好用数据的唯一 ID。

  • getData 里返回的数据类型要和模板里声明的一致。

13.4 实战:文章卡片列表

把 List、LazyForEach、之前学的 Flex 布局合在一起,做一个文章列表。左边封面图占位,右边标题和摘要,每条之间分割线隔开。

// 文章数据模型type Article = {  id: string;  title: string;  summary: string;  author: string;  readCount: number;};// 数据源classArticleDataSourceimplementsIDataSource{  private articles: Article[] = [];  constructor(articles: Article[]) {    this.articles = articles;  }  totalCount(): number {    return this.articles.length;  }  getData(index: number): Article {    return this.articles[index];  }  registerDataChangeListener(): void {}  unregisterDataChangeListener(): void {}}@Entry@Componentstruct ArticleListPage {  @State dataSource: ArticleDataSource = new ArticleDataSource([    { id: '1', title: '鸿蒙原生应用开发实战:从零搭建一个完整App', summary: '分享实战代码、源码解析、性能优化及生态前沿', author: '华鸿技术栈', readCount: 1280 },    { id: '2', title: 'ArkTS 声明式 UI 入门:扔掉命令式的旧思维', summary: '命令式 vs 声明式,build() 方法,链式调用,手写计数器组件', author: '华鸿技术栈', readCount: 960 },    { id: '3', title: '@State 状态管理详解:数据变了,UI 自动刷新', summary: '@State 原理,同值不重画,对象和数组的整体替换规则', author: '华鸿技术栈', readCount: 1560 },    { id: '4', title: '@Prop 和 @Link:父子组件传值的正确写法', summary: '单向绑定和双向绑定怎么选,$ 前缀的作用,常见错误排查', author: '华鸿技术栈', readCount: 820 },    { id: '5', title: 'Flex 弹性布局:Column 和 Row 的对齐规则', summary: '主轴交叉轴怎么对齐,space 控制间距,layoutWeight 弹性分配', author: '华鸿技术栈', readCount: 1100 },  ]);  build() {    List() {      LazyForEach(this.dataSource, (item: Article) => {        ListItem() {          Column() {            Row() {              // 左侧封面图占位              Column()                .width(100)                .height(75)                .backgroundColor('#E8F0FE')                .borderRadius(8)                .justifyContent(FlexAlign.Center)              // 右侧文字区域              Column({ space: 6 }) {                Text(item.title)                  .fontSize(16)                  .fontWeight(FontWeight.Medium)                  .fontColor('#333333')                  .maxLines(2)                  .textOverflow({ overflow: TextOverflow.Ellipsis })                Text(item.summary)                  .fontSize(13)                  .fontColor('#999999')                  .maxLines(2)                  .textOverflow({ overflow: TextOverflow.Ellipsis })                Row() {                  Text(item.author)                    .fontSize(12)                    .fontColor('#666666')                  Text(' · ')                    .fontSize(12)                    .fontColor('#CCCCCC')                  Text(`${item.readCount}阅读`)                    .fontSize(12)                    .fontColor('#666666')                }              }              .layoutWeight(1)              .margin({ left: 12 })              .alignItems(HorizontalAlign.Start)            }            .width('100%')            .padding(16)            Divider()              .margin({ left: 16 })          }        }      })    }  }}

运行起来就是一个完整的文章列表。封面图占位用了一个带背景色的 Column,后面换成 Image 组件加载网络图片就行,思路跟第10篇讲的一样。

每条的结构:Row 里左边封面图占位、右边标题摘要作者阅读量,最下面一条分割线。这个结构在后面做 App 首页文章流的时候会直接用。

13.5 列表性能的几个注意点

ForEach vs LazyForEach:数据少(几十条以内)用 ForEach,写法简单。数据多或者不确定数据量用 LazyForEach,只渲染可见区域的列表项,滑出屏幕的回收掉,内存和性能都好很多。

key 的作用:ForEach 和 LazyForEach 都有 key 参数。框架靠 key 判断哪些列表项是新增的、哪些是删掉的、哪些位置变了。默认用 index 当 key,如果列表会动态增删数据,最好用数据的唯一 ID 当 key,避免 key 重复导致渲染错乱。

ListItem 里别放太重的东西:每一项的 UI 尽量精简,图片用懒加载,复杂计算放到数据层提前做完,别在 build 里做耗时操作。

13.6 本篇总结

鸿蒙开发中列表三件套:

  • List:滚动列表容器,支持竖直和水平方向。

  • ForEach:循环渲染,数据量少的时候用,写法简单。

  • LazyForEach:懒加载,只渲染可见区域的列表项,数据多的时候用。

常见问题

Q: List 和 ForEach 一定要一起用吗?A: 不一定。ForEach 可以放在 Column 里做非滚动的循环渲染。但如果数据多需要滚动,就得放在 List 里。

Q: ForEach 和 LazyForEach 怎么选?A: 数据量少(几十条以内)用 ForEach,写法简单。数据量多或者不确定用 LazyForEach。提前换成 LazyForEach 没有额外代价。

Q: LazyForEach 的 IDataSource 必须实现那四个方法吗?A: totalCount 和 getData 必须实现,框架靠这两个方法知道有多少数据、每项渲染什么。registerDataChangeListener 和 unregisterDataChangeListener 暂时空着。

Q: key 默认用 index 有什么问题?A: 列表数据顺序不变的话没问题。如果列表会增删改,最好用数据的唯一 ID 当 key,避免渲染错乱。

下一篇:学习了列表渲染,但数据是本地的。下一篇讲网络请求——http 模块,async/await 怎么处理异步,把文章列表换成从服务器获取的真实数据。

本文由「华鸿技术栈」原创发布,专注鸿蒙原生应用与元服务开发。分享实战代码、源码解析、性能优化及生态前沿。不讲虚的,只上干货。转载请联系授权。

#鸿蒙开发 #ArkTS #ArkUI #List #ForEach #LazyForEach #鸿蒙原生应用开发 #鸿蒙培训

最新文章

随机文章