业务开发里几乎都会遇到长列表:商品列表、消息列表、订单列表。很多新手第一反应直接用ForEach套 Column,一次性渲染全部数据。数据少看着没问题,一旦上千条数据,页面直接卡顿、滑动掉帧、内存飙升,甚至 App 闪退。

鸿蒙 7 提供List组件,自带懒加载(按需渲染),只有进入屏幕可视区域的 Item 才会创建渲染,滑出去的 Item 会回收复用。这就是长列表首选方案。

误区:List ≠ 简单的竖向排列。普通 Column+ForEach 是一次性渲染所有节点,没有复用;List 自带 Item 复用池,大数据量下性能差距巨大。

今天我们从基础 List 写法、ListItem、列表分割线,再到懒加载原理、下拉刷新、上拉加载更多分页,全部用鸿蒙 7 可直接运行代码演示,一并讲清长列表最常见的性能坑。

一、基础 List + ListItem 最简示例

List 是容器,ListItem代表列表里面每一行条目。List 默认垂直排列。

​
@Entry
@Component
struct ListSimpleDemo {
  // 模拟列表数据源
  @State dataList: string[] = ["条目1", "条目2", "条目3", "条目4", "条目5"];

  build() {
    Column() {
      List() {
        ForEach(this.dataList, (item: string) => {
          ListItem() {
            Text(item)
              .fontSize(18)
              .padding(16)
          }
        }, (item: string) => item)
      }
      .width('100%')
      .height('100%')
    }
  }
}

​
代码拆解
  1. List:列表容器,必须设置高度(固定 / 百分比),不设置高度 List 无法滚动、懒加载失效。
  2. ListItem:列表每一行的根节点,List 内部只能放 ListItem,不能直接放 Text、Button。
  3. ForEach 第三个参数:key生成函数,必须提供唯一 key,用于 Item 复用,key 重复会出现 UI 错乱。

重点坑:忘记给 List 设置 height。很多新手写 List 不限制高度,List 会一次性渲染全部数据,懒加载直接失效。

二、美化列表:分割线、间距、点击事件

真实列表一般需要分割线,条目点击跳转。

​
@Entry
@Component
struct ListItemStyleDemo {
  @State dataList: string[] = ["苹果", "香蕉", "橙子", "葡萄", "芒果"];

  build() {
    Column() {
      List() {
        ForEach(this.dataList, (item: string) => {
          ListItem() {
            Row() {
              Text(item)
                .fontSize(18)
            }
            .width('100%')
            .padding(16)
          }
          .onClick(()=>{
            console.info("点击了:" + item)
          })
        }, (item:string)=>item)
      }
      .width('100%')
      .height('100%')
      .divider({ strokeWidth:1, color:"#eee" }) // 分割线
    }
  }
}

​
  • .divider():快速添加列表分割线,strokeWidth 控制线粗细;
  • 点击事件写在 ListItem 上,整行可点击。

三、上拉加载更多(分页加载)

业务最常用场景:滚动到底部,请求下一页数据。 List 提供onScrollIndex监听可视条目索引,判断是否滑到最后一条,触发加载下一页。

​
@Entry
@Component
struct ListLoadMoreDemo {
  @State dataList: number[] = [];
  @State page: number = 1;
  @State isLoading: boolean = false;

  aboutToAppear() {
    this.loadData()
  }

  // 模拟接口请求分页数据
  loadData() {
    if(this.isLoading) return;
    this.isLoading = true;
    // 模拟网络延迟
    setTimeout(()=>{
      const temp: number[] = []
      for(let i=0; i<10;i++){
        temp.push((this.page-1)*10 + i +1)
      }
      this.dataList = [...this.dataList, ...temp]
      this.page +=1
      this.isLoading = false
    }, 800)
  }

  build() {
    Column() {
      List() {
        ForEach(this.dataList, (num:number)=>{
          ListItem(){
            Text(`列表条目 ${num}`)
              .padding(16)
              .fontSize(18)
          }
        }, (num:number)=>num.toString())
        // 底部加载更多提示
        ListItem(){
          if(this.isLoading){
            Text("正在加载更多...")
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding(16)
          }else{
            Text("上拉加载更多")
              .width('100%')
              .textAlign(TextAlign.Center)
              .padding(16)
          }
        }
      }
      .width('100%')
      .height('100%')
      // 监听列表滚动索引
      .onScrollIndex((start:number, end:number)=>{
        // end是当前屏幕最后一条索引,等于数组最后下标,触发加载
        if(end >= this.dataList.length - 1 && !this.isLoading){
          this.loadData()
        }
      })
    }
  }
}

​
关键逻辑说明
  1. isLoading锁:防止短时间多次触发加载,重复请求接口;
  2. onScrollIndex回调,拿到屏幕可见条目的首尾索引;
  3. 滑到最后一条,调用加载函数追加数据;
  4. 追加数据时用...展开生成新数组,触发 List 刷新。

四、下拉刷新 List 自带下拉刷新

鸿蒙 7 List 内置下拉刷新,不用自己手写动画。

​
@Entry
@Component
struct ListRefreshDemo {
  @State dataList: number[] = [1,2,3,4,5];
  @State isRefreshing: boolean = false;

  // 下拉刷新回调
  onRefresh() {
    this.isRefreshing = true
    setTimeout(()=>{
      // 模拟重新请求第一页数据
      this.dataList = [1,2,3,4,5]
      this.isRefreshing = false
    }, 1000)
  }

  build() {
    Column() {
      List() {
        ForEach(this.dataList, (num:number)=>{
          ListItem(){
            Text(`条目 ${num}`).padding(16)
          }
        }, num=>num.toString())
      }
      .width('100%')
      .height('100%')
      .refresh({
        refreshing: this.isRefreshing,
        onRefresh: ()=> this.onRefresh()
      })
    }
  }
}

​
  • refresh属性开启下拉刷新;
  • refreshing绑定状态,true 展示 loading 转圈,加载完成置 false;
  • 下拉松手触发 onRefresh 回调,在这里重新拉取首页数据。

五、List 开发高频踩坑汇总

坑 1:List 没有设置 height,懒加载失效

List 必须有确定高度,100% 或者固定 px,否则 List 会一次性渲染全部条目,大数据直接卡顿。

坑 2:ForEach key 重复,列表 UI 错乱

第三个参数生成唯一 key,不能直接用数组索引 index 当 key。删除、新增数组元素时,index 复用会出现文字图片错位。 ❌错误写法:ForEach(arr, item=>{}, (item,index)=>index)

坑 3:ListItem 内部组件太复杂,Item 复用带来状态错乱

ListItem 内 @State 变量,滑动复用会保留旧状态。解决方案:列表数据全部放到外层数据源,ListItem 内部尽量不维护独立状态。

坑 4:上拉加载多次触发

没有 isLoading 加载锁,快速滑动到底部会连续多次请求接口,造成重复数据。加载中直接 return 拦截。

坑 5:长列表图片不做缓存,滑动卡顿

列表内 Image 组件建议设置.cached(true)开启图片缓存,减少重复解码开销。

六、List 功能选型速查表

表格

功能API适用场景
基础滚动列表List + ListItem + ForEach订单、消息、商品列表
分割线.divider()条目之间分隔
下拉刷新.refresh()下拉重新加载首页数据
上拉分页加载onScrollIndex 监听索引滚动到底部加载下一页

结尾总结

List 是鸿蒙 7 长列表首选组件,核心优势是 Item 复用、懒加载,千万不要用 Column+ForEach 写上千条数据。

  1. List 必须设置高度,内部只能放 ListItem;
  2. ForEach 一定要提供稳定唯一 key,不要用数组下标;
  3. 分页记得加加载锁,避免重复请求;
  4. 内置 refresh 实现下拉刷新,onScrollIndex 监听滚动实现上拉加载;
  5. ListItem 内部尽量少维护本地状态,防止条目复用时 UI 错乱。

实操指南

结合本篇代码,实现一个同时支持下拉刷新 + 上拉加载更多的列表,每条 Item 展示序号,加载时显示 loading 提示。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐