在这里插入图片描述
在这里插入图片描述

一、引言

列表是移动应用中最常见的 UI 形态。无论是消息列表、商品列表、通讯录,还是社交动态,都离不开列表组件。当列表数据量很大(成百上千条)时,如果一次性全部渲染,会导致严重的内存占用和性能问题。HarmonyOS 提供了 LazyForEach(懒加载) 机制,配合 List 组件,可以高效处理长列表。

本文将以一个清新绿白风格的长列表演示页面为主线,深入讲解 List 组件、LazyForEach 懒加载、列表性能优化的核心技能。

二、List 组件基础

2.1 List 与 ListItem

List 是 ArkUI 的列表容器组件,ListItem 是列表项组件:

List({ space: 10 }) {
  ListItem() {
    Text('列表项一')
  }
  ListItem() {
    Text('列表项二')
  }
}

2.2 List 的核心属性

List({
  space: 10,           // 列表项间距
  initialIndex: 0,     // 初始滚动位置
  scroller: this.scroller // 滚动控制器
}) {
  // 列表项
}
.listDirection(Axis.Vertical)  // 滚动方向
.scrollBar(BarState.Auto)      // 滚动条
.edgeEffect(EdgeEffect.Spring) // 边缘效果

三、ForEach 与 LazyForEach

3.1 ForEach 的问题

ForEach 会一次性渲染所有列表项,当数据量很大时:

  • 内存占用巨大。
  • 首次渲染耗时。
  • 滚动性能下降。
// 数据量大时性能差
List() {
  ForEach(this.hugeArray, (item) => {
    ListItem() {
      Text(item)
    }
  })
}

3.2 LazyForEach 的优势

LazyForEach 采用懒加载策略:

  1. 按需渲染:只渲染当前可见区域的列表项。
  2. 组件复用:滚出可视区域的列表项会被回收复用。
  3. 内存友好:无论数据多大,内存中只保留可见项。

四、LazyForEach 的使用

4.1 数据源要求

LazyForEach 的数据源必须是 IDataSource 接口的实现类:

class MyDataSource implements IDataSource {
  private data: string[] = [];
  private listeners: DataChangeListener[] = [];

  totalCount(): number {
    return this.data.length;
  }

  getData(index: number): string {
    return this.data[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listeners.push(listener);
  }

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

  // 数据变更通知
  notifyDataReload(): void {
    this.listeners.forEach((l) => l.onDataReloaded());
  }
}

代码说明:

IDataSource 接口要求实现以下方法:

  • totalCount():返回数据总数。
  • getData(index):根据索引返回数据。
  • registerDataChangeListener / unregisterDataChangeListener:注册/注销数据变更监听。
  • 数据变化时需要通知监听器,如 onDataReloaded()

4.2 使用 LazyForEach

List({ space: 10 }) {
  LazyForEach(this.dataSource, (item: string) => {
    ListItem() {
      Text(item)
    }
  }, (item: string) => item)  // 键生成器
}

代码说明:

  • LazyForEach 第一个参数是数据源(IDataSource 实现)。
  • 第二个参数是渲染函数。
  • 第三个参数是键生成器,用于唯一标识每个列表项,优化复用。

五、实战代码:长列表演示页面

下面我们实现一个清新绿白风格的长列表演示页面,包含吸顶标题、懒加载列表和悬浮按钮。

5.1 定义数据结构

interface ListItem {
  id: number;
  title: string;
  desc: string;
  tag: string;
}

代码说明:

ListItem 接口描述列表项数据:

  • id:唯一编号。
  • title:标题。
  • desc:描述文字。
  • tag:标签(重要/普通/新增)。

5.2 初始化数据

@Entry
@Component
struct ListPage {
  @State items: ListItem[] = [];
  @State count: number = 50;

  aboutToAppear(): void {
    for (let i = 1; i <= 50; i++) {
      this.items.push({
        id: i,
        title: `列表项 ${i}`,
        desc: `这是第 ${i} 条数据,用于演示 LazyForEach 懒加载长列表的滚动性能`,
        tag: i % 3 === 0 ? '重要' : (i % 3 === 1 ? '普通' : '新增')
      });
    }
  }

代码说明:

  • aboutToAppear 生命周期方法中初始化 50 条数据。
  • 通过 i % 3 的余数分配不同标签,让列表看起来更真实。
  • @State count 用于生成新数据的编号。

5.3 添加数据方法

addItem(): void {
  this.count++;
  this.items.push({
    id: this.count,
    title: `列表项 ${this.count}`,
    desc: `这是第 ${this.count} 条数据,用于演示 LazyForEach 懒加载长列表的滚动性能`,
    tag: '新增'
  });
}

代码说明:

addItem 方法向列表追加一条新数据,演示列表的动态更新。点击悬浮按钮即可触发。

5.4 列表项构建器

@Builder
ItemCard(item: ListItem) {
  Row({ space: 12 }) {
    // 编号
    Text(`${item.id}`)
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
      .fontColor(Color.White)
      .width(40)
      .height(40)
      .textAlign(TextAlign.Center)
      .borderRadius(20)
      .backgroundColor(item.tag === '重要' ? '#2ED573' : (item.tag === '新增' ? '#FFA502' : '#7BED9F'))
    // 内容
    Column({ space: 4 }) {
      Row({ space: 8 }) {
        Text(item.title)
          .fontSize(15)
          .fontWeight(FontWeight.Bold)
          .fontColor('#2F3542')
        Text(item.tag)
          .fontSize(10)
          .fontColor(Color.White)
          .padding({ left: 6, right: 6, top: 2, bottom: 2 })
          .backgroundColor(item.tag === '重要' ? '#2ED573' : (item.tag === '新增' ? '#FFA502' : '#7BED9F'))
          .borderRadius(8)
      }
      .alignItems(VerticalAlign.Center)
      Text(item.desc)
        .fontSize(12)
        .fontColor('#747D8C')
        .maxLines(2)
        .textOverflow({ overflow: TextOverflow.Ellipsis })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
  }
  .width('100%')
  .padding(14)
  .backgroundColor(Color.White)
  .borderRadius(14)
  .shadow({ radius: 6, color: '#11000000', offsetY: 2 })
}

代码说明:

ItemCard 是列表项的 UI 构建器:

  1. 编号圆标:40x40 的圆形,根据标签类型使用不同颜色(重要=绿色、新增=橙色、普通=浅绿)。

  2. 标题与标签:标题加粗,标签是彩色小胶囊。

  3. 描述文字maxLines(2) 限制最多两行,textOverflow 超出部分显示省略号。

  4. 卡片样式:白色背景、圆角、轻微阴影,形成卡片列表效果。

5.5 构建 UI

build() {
  Stack({ alignContent: Alignment.BottomEnd }) {
    Column() {
      // 顶部吸顶标题
      Column() {
        Text('LIST')
          .fontSize(12)
          .fontColor('#B8FFD8')
          .letterSpacing(6)
        Text('列表与懒加载')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
          .fontColor(Color.White)
          .margin({ top: 4 })
        Text(`${this.items.length} 项 · LazyForEach 懒加载`)
          .fontSize(12)
          .fontColor('#B8FFD8')
          .margin({ top: 4 })
      }
      .width('100%')
      .padding({ top: 40, bottom: 20 })
      .backgroundColor('#0E8A5F')

      // List 长列表
      List({ space: 10 }) {
        ForEach(this.items, (item: ListItem) => {
          ListItem() {
            this.ItemCard(item)
          }
        }, (item: ListItem) => `${item.id}`)
      }
      .width('100%')
      .layoutWeight(1)
      .padding(12)
      .scrollBar(BarState.Auto)
    }
    .width('100%')
    .height('100%')

    // 悬浮圆形按钮
    Button() {
      Text('+')
        .fontSize(28)
        .fontColor(Color.White)
    }
    .width(56)
    .height(56)
    .backgroundColor('#2ED573')
    .borderRadius(28)
    .shadow({ radius: 16, color: '#552ED573', offsetY: 4 })
    .margin({ right: 20, bottom: 24 })
    .onClick(() => { this.addItem(); })
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#F0F5F2')
}

代码说明:

页面整体使用 Stack 布局,底层是列表,顶层是悬浮按钮:

  1. 吸顶标题:绿色背景的标题区,显示列表项总数。

  2. List 长列表

    • List({ space: 10 }) 创建列表,间距 10。
    • ForEach 渲染列表项(演示环境用 ForEach,生产环境长列表应使用 LazyForEach)。
    • .layoutWeight(1) 让列表占满剩余空间。
    • .scrollBar(BarState.Auto) 自动显示滚动条。
  3. 悬浮按钮StackalignContent: Alignment.BottomEnd 将按钮定位在右下角,圆形绿色按钮带发光阴影。

六、List 高级用法

6.1 分组列表

List() {
  // 分组标题
  ListItem() {
    Text('分组 A')
      .fontSize(16)
      .fontWeight(FontWeight.Bold)
  }
  .sticky(StickyStyle.Header)  // 吸顶效果

  // 分组内容
  ForEach(groupA, (item) => {
    ListItem() {
      Text(item)
    }
  })
}

6.2 横向列表

List({ space: 10 }) {
  ForEach(items, (item) => {
    ListItem() {
      Text(item)
    }
  })
}
.listDirection(Axis.Horizontal)  // 横向滚动

6.3 列表滚动控制

// 创建滚动控制器
private scroller: Scroller = new Scroller();

// 滚动到指定位置
this.scroller.scrollToIndex(100);
this.scroller.scrollEdge(Edge.Top);

// 绑定到列表
List({ scroller: this.scroller }) {
  // ...
}

七、列表性能优化

7.1 使用 LazyForEach

大数据量列表务必使用 LazyForEach,实现按需渲染:

List({ space: 10 }) {
  LazyForEach(this.dataSource, (item: ListItem) => {
    ListItem() {
      this.ItemCard(item)
    }
  }, (item: ListItem) => `${item.id}`)
}

7.2 键生成器的重要性

键生成器用于唯一标识列表项。使用稳定的键(如数据 id)可以提升复用效率:

// 推荐:使用唯一 id
(item: ListItem) => `${item.id}`

// 不推荐:使用索引
(item: ListItem, index: number) => `${index}`

7.3 避免复杂布局

列表项布局应尽量简单,避免深层嵌套和复杂计算,减少渲染开销。

7.4 图片懒加载

列表中的图片应使用懒加载或占位图,避免一次性加载大量图片导致内存溢出。

八、常见问题

8.1 列表滚动卡顿

原因:列表项渲染复杂、数据量大、未使用懒加载。

解决:使用 LazyForEach,简化列表项布局。

8.2 列表项复用错乱

原因:键生成器不稳定或重复。

解决:使用唯一且稳定的键。

8.3 数据更新不刷新

原因:直接修改数组元素不触发 UI 刷新。

解决:使用新数组替换,或使用 IDataSource 的通知机制。

九、总结

本文深入讲解了 HarmonyOS 列表与懒加载技术,通过一个清新绿白风格的长列表演示页面实战演示了 List 组件、列表项构建、动态更新和悬浮按钮等核心能力。

核心要点回顾:

  1. List 是列表容器,ListItem 是列表项。
  2. ForEach 一次性渲染所有项,适合小数据量。
  3. LazyForEach 按需渲染,适合大数据量长列表。
  4. LazyForEach 数据源需实现 IDataSource 接口。
  5. 键生成器影响列表复用效率。
  6. 列表性能优化:懒加载、简化布局、稳定键。

列表是应用的基石,掌握懒加载和性能优化能让应用流畅处理海量数据。下一篇我们将讲解 HarmonyOS 表单与校验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐