首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >[鸿蒙从零到一] ArkUI 列表与网格实战:List、Grid 与 LazyForEach

[鸿蒙从零到一] ArkUI 列表与网格实战:List、Grid 与 LazyForEach

原创
作者头像
hunter android
发布2026-07-20 11:10:36
发布2026-07-20 11:10:36
300
举报

ArkUI 列表与网格实战:List、Grid 与 LazyForEach

移动应用中,大量页面都可以归结为“展示一组数据”:消息流适合纵向列表,频道入口适合网格,商品页面还可能同时包含分组、滚动定位、增删更新和空状态。

ArkUI 提供了 ListGridForEachLazyForEach 等能力。组件本身并不难,真正影响体验的是如何选择容器、怎样建立稳定的数据标识、何时使用懒加载,以及数据变化后如何准确通知界面。

本文从静态列表开始,逐步实现一个支持网格切换、数据增删和滚动定位的内容页面,并梳理常见性能问题。

一、List 与 Grid 分别解决什么问题

List 适合单轴滚动的数据集合,常见场景包括消息、联系人、设置项、新闻流和横向推荐内容。Grid 适合按行列排布内容,常见于相册、商品、应用入口和文件缩略图。

两者的直接子节点也不同:List 中使用 ListItemGrid 中使用 GridItem。不要跳过这一层直接放业务组件,否则滚动容器无法正确管理子项。

二、用 List 构建基础列表

先定义页面需要的数据模型:

代码语言:javascript
复制
class ArticleItem {
  id: string
  title: string
  category: string
  readCount: number

  constructor(id: string, title: string, category: string, readCount: number) {
    this.id = id
    this.title = title
    this.category = category
    this.readCount = readCount
  }
}

使用 ForEach 可以快速渲染一组规模较小的数据:

代码语言:javascript
复制
@Entry
@Component
struct ArticleListPage {
  @State articles: ArticleItem[] = [
    new ArticleItem('arkui-layout', 'ArkUI 布局实践', 'ArkUI', 1280),
    new ArticleItem('stage-model', 'Stage 模型解析', '工程化', 960),
    new ArticleItem('network-request', '网络请求与错误处理', '网络', 1560)
  ]

  build() {
    Column() {
      Text('技术文章')
        .fontSize(24)
        .fontWeight(FontWeight.Bold)
        .width('100%')
        .padding({ left: 16, top: 16, bottom: 12 })

      List({ space: 12 }) {
        ForEach(this.articles, (item: ArticleItem) => {
          ListItem() {
            ArticleRow({ item: item })
          }
        }, (item: ArticleItem) => item.id)
      }
      .width('100%')
      .layoutWeight(1)
      .padding({ left: 16, right: 16, bottom: 16 })
    }
    .width('100%')
    .height('100%')
  }
}

@Component
struct ArticleRow {
  item: ArticleItem

  build() {
    Row() {
      Column({ space: 8 }) {
        Text(this.item.title)
          .fontSize(17)
          .fontWeight(FontWeight.Medium)
          .maxLines(2)
          .textOverflow({ overflow: TextOverflow.Ellipsis })

        Text(`${this.item.category} · ${this.item.readCount} 次阅读`)
          .fontSize(13)
          .fontColor('#6B7280')
      }
      .alignItems(HorizontalAlign.Start)
      .layoutWeight(1)
    }
    .width('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(8)
  }
}

Listspace 用于设置列表项间距。页面给列表设置 layoutWeight(1),让它占据标题之外的剩余空间,同时获得明确的可滚动区域。

ForEach 的末尾参数是键生成函数。这里使用业务数据中的 id,框架就能识别某条数据是新增、删除、移动还是保持不变。

三、稳定的 key 为什么重要

下面这种写法看似方便,却不适合会增删或重排的数据:

代码语言:javascript
复制
ForEach(this.articles, (item: ArticleItem) => {
  ListItem() {
    ArticleRow({ item: item })
  }
}, (item: ArticleItem, index: number) => index.toString())

删除中间元素后,后续所有元素的索引都会变化。框架可能将原有节点错误地复用给另一条数据,输入状态、动画状态或子组件内部状态也可能跟着错位。

键值应在同一集合内唯一,在数据生命周期内稳定,并能直接表示业务实体身份。服务端主键、本地生成的 UUID、不可变的组合键都比数组索引更合适。标题、昵称等可能重复或变化的字段也不宜单独作为键值。

四、长列表使用 LazyForEach

ForEach 会为当前数据集合创建对应的子组件。数据较多时,可以使用 LazyForEach 按需创建可见区域附近的节点,离开缓存范围的节点可被框架回收。

LazyForEach 需要实现 IDataSource。一个可复用的基础数据源如下:

代码语言:javascript
复制
class ArticleDataSource implements IDataSource {
  private items: ArticleItem[] = []
  private listeners: DataChangeListener[] = []

  totalCount(): number {
    return this.items.length
  }

  getData(index: number): ArticleItem {
    return this.items[index]
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener)
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const index = this.listeners.indexOf(listener)
    if (index >= 0) {
      this.listeners.splice(index, 1)
    }
  }

  replaceAll(items: ArticleItem[]): void {
    this.items = items
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataReloaded()
    })
  }

  add(item: ArticleItem): void {
    const index = this.items.length
    this.items.push(item)
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataAdd(index)
    })
  }

  update(index: number, item: ArticleItem): void {
    if (index < 0 || index >= this.items.length) {
      return
    }
    this.items[index] = item
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataChange(index)
    })
  }

  remove(index: number): void {
    if (index < 0 || index >= this.items.length) {
      return
    }
    this.items.splice(index, 1)
    this.listeners.forEach((listener: DataChangeListener) => {
      listener.onDataDelete(index)
    })
  }
}

页面持有数据源后,可以直接交给 LazyForEach

代码语言:javascript
复制
@Entry
@Component
struct LazyArticlePage {
  private dataSource: ArticleDataSource = new ArticleDataSource()

  aboutToAppear(): void {
    const items: ArticleItem[] = []
    for (let index = 0; index < 1000; index++) {
      items.push(new ArticleItem(
        `article-${index}`,
        `ArkTS 实战内容 ${index + 1}`,
        index % 2 === 0 ? 'ArkUI' : '工程化',
        500 + index
      ))
    }
    this.dataSource.replaceAll(items)
  }

  build() {
    List({ space: 10 }) {
      LazyForEach(this.dataSource, (item: ArticleItem) => {
        ListItem() {
          ArticleRow({ item: item })
        }
      }, (item: ArticleItem) => item.id)
    }
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

这里最容易遗漏的是数据变化通知。仅修改 items 数组并不能完整表达变化类型,数据源必须调用对应的监听方法:

数据操作

通知方法

适用情况

全量替换

onDataReloaded()

刷新、重新排序、条件整体变化

新增一项

onDataAdd(index)

插入或追加数据

更新一项

onDataChange(index)

指定位置内容变化

删除一项

onDataDelete(index)

指定位置数据移除

能准确描述局部变化时,应优先发送局部通知。频繁调用全量刷新会增加不必要的组件重建和布局计算。

五、用 Grid 实现自适应网格

相同的数据可以切换为网格展示。columnsTemplate 用于声明列轨道:

代码语言:javascript
复制
@Component
struct ArticleGrid {
  dataSource: ArticleDataSource

  build() {
    Grid() {
      LazyForEach(this.dataSource, (item: ArticleItem) => {
        GridItem() {
          Column({ space: 10 }) {
            Text(item.category)
              .fontSize(12)
              .fontColor('#2563EB')
              .width('100%')

            Text(item.title)
              .fontSize(16)
              .fontWeight(FontWeight.Medium)
              .maxLines(2)
              .textOverflow({ overflow: TextOverflow.Ellipsis })
              .width('100%')

            Text(`${item.readCount} 次阅读`)
              .fontSize(12)
              .fontColor('#6B7280')
              .width('100%')
          }
          .alignItems(HorizontalAlign.Start)
          .justifyContent(FlexAlign.SpaceBetween)
          .width('100%')
          .height(140)
          .padding(14)
          .backgroundColor('#FFFFFF')
          .borderRadius(8)
        }
      }, (item: ArticleItem) => item.id)
    }
    .columnsTemplate('1fr 1fr')
    .columnsGap(12)
    .rowsGap(12)
    .width('100%')
    .height('100%')
    .padding(16)
  }
}

1fr 1fr 表示等宽两列。在平板、折叠屏等大窗口上,可以结合窗口断点将模板切换为三列或四列。业务应依据最小卡片宽度统一制定适配规则,避免每个页面维护一套互相冲突的阈值。

六、滚动控制与定位

需要“回到顶部”或跳转到指定位置时,可以为列表绑定 Scroller

代码语言:javascript
复制
@Entry
@Component
struct ScrollableArticlePage {
  private scroller: Scroller = new Scroller()
  private dataSource: ArticleDataSource = new ArticleDataSource()
  @State showBackToTop: boolean = false

  build() {
    Stack({ alignContent: Alignment.BottomEnd }) {
      List({ space: 10, scroller: this.scroller }) {
        LazyForEach(this.dataSource, (item: ArticleItem) => {
          ListItem() {
            ArticleRow({ item: item })
          }
        }, (item: ArticleItem) => item.id)
      }
      .onScrollIndex((start: number) => {
        this.showBackToTop = start > 8
      })

      if (this.showBackToTop) {
        Button('回到顶部')
          .margin(20)
          .onClick(() => {
            this.scroller.scrollToIndex(0, true, ScrollAlign.Start)
          })
      }
    }
    .width('100%')
    .height('100%')
  }
}

滚动回调触发频率较高,不适合直接执行网络请求、复杂计算或频繁写入持久化存储。业务只需从回调中提取必要状态,重任务应做节流或移到其他时机。

七、加载态、空态与错误态

完整的数据页面不能只处理成功列表。可以先将页面状态明确建模:

代码语言:javascript
复制
enum LoadStatus {
  Loading,
  Success,
  Empty,
  Error
}

@Component
struct ArticleContent {
  @State status: LoadStatus = LoadStatus.Loading
  private dataSource: ArticleDataSource = new ArticleDataSource()

  build() {
    Stack() {
      if (this.status === LoadStatus.Loading) {
        LoadingProgress()
          .width(36)
          .height(36)
      } else if (this.status === LoadStatus.Empty) {
        Text('暂无内容')
          .fontColor('#6B7280')
      } else if (this.status === LoadStatus.Error) {
        Column({ space: 12 }) {
          Text('加载失败,请稍后重试')
          Button('重新加载')
            .onClick(() => this.loadData())
        }
      } else {
        List() {
          LazyForEach(this.dataSource, (item: ArticleItem) => {
            ListItem() {
              ArticleRow({ item: item })
            }
          }, (item: ArticleItem) => item.id)
        }
      }
    }
    .width('100%')
    .height('100%')
  }

  private loadData(): void {
    this.status = LoadStatus.Loading
    // 请求完成后更新数据源,并将状态切换为 Success、Empty 或 Error
  }
}

将加载状态与数据集合分开后,页面逻辑会更清晰。空数组不再同时承担“尚未请求”“请求失败”和“确实没有数据”三种含义。

八、常见问题与性能建议

列表没有滚动

滚动容器必须获得受约束的尺寸。如果 List 放在 Column 中却没有高度或 layoutWeight,其内容可能按完整高度参与布局,自然不会形成滚动区域。

数据变化但界面没有更新

使用普通数组时,要确认它处于可观察状态,并通过可被框架识别的方式更新。使用 LazyForEach 时,则要确认数据源发送了与操作一致的通知。

滚动过程中明显掉帧

可以依次检查以下位置:

  • 子项 build() 中是否执行排序、过滤、JSON 解析等重复计算;
  • 是否为每个子项同步解码大图;
  • key 是否稳定,是否造成大量节点失效;
  • 是否在滚动回调中高频修改多个状态;
  • 子项层级是否过深,是否存在不必要的透明叠加和阴影;
  • 是否在长数据集合上继续使用一次性构建的 ForEach

展示模型应尽量在进入 UI 层前准备好。列表子项负责渲染与轻量交互,不应承担数据清洗和复杂业务编排。

所有更新都调用全量刷新

全量刷新容易实现,但会失去局部更新优势。新增、删除、修改能够定位到具体索引时,应发出对应通知;只有整体替换、排序或过滤结果彻底变化时再重新加载全部数据。

九、组件选择清单

  • 数据量小、内容简单且基本不变化:ListGrid 配合 ForEach
  • 数据量较大,需要按需创建节点:ListGrid 配合 LazyForEach
  • 单列或单行滚动:优先 List
  • 多列卡片和缩略图:优先 Grid
  • 数据会频繁增删:实现稳定 key,并发送精确的数据源通知;
  • 需要定位与回顶:绑定 Scroller
  • 页面来自网络:显式处理加载、成功、空数据和错误状态。

总结

ArkUI 集合页面的核心不只是把数据循环出来,而是让容器、数据身份和更新机制彼此匹配。

List 负责单轴滚动,Grid 负责行列布局;ForEach 适合简单的小规模集合,LazyForEach 通过按需创建节点支撑更长的数据。稳定的 key 保证节点身份正确,精确的数据变化通知减少无效重建,明确的页面状态则让加载和异常流程保持可维护。

掌握这些原则后,无论是消息流、商品网格还是文件列表,都可以在同一套思路下完成建模,并在数据增长时保持稳定的滚动体验。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ArkUI 列表与网格实战:List、Grid 与 LazyForEach
    • 一、List 与 Grid 分别解决什么问题
    • 二、用 List 构建基础列表
    • 三、稳定的 key 为什么重要
    • 四、长列表使用 LazyForEach
    • 五、用 Grid 实现自适应网格
    • 六、滚动控制与定位
    • 七、加载态、空态与错误态
    • 八、常见问题与性能建议
      • 列表没有滚动
      • 数据变化但界面没有更新
      • 滚动过程中明显掉帧
      • 所有更新都调用全量刷新
    • 九、组件选择清单
    • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档