HarmonyOS开发:长列表界面实现详解(使用懒加载)

目录

  • 前言
  • 长列表界面开发挑战
  • 关于懒加载
  • HarmonyOS中的LazyForEach
  • 组件的创建
  • 关于长列表拖拽排序
  • 番外篇:NodeAdapter使用
  • 结束语

前言

随着大数据的快速发展,在移动应用开发中,多数据的长列表是非常常见的情况,需要允许用户浏览大量的数据项,比如商品列表、新闻资讯等。但是长列表的加载和渲染往往对性能要求较高,处理不当可能导致内存溢出或界面卡顿,这里就不得不提懒加载操作,因为它是一种非常有效的解决方案,可以按需加载数据,从而提高应用性能和用户体验。那么本文就来详细介绍在HarmonyOS开发中如何实现长列表界面的开发需求,并运用懒加载技术来处理,分享给大家,希望能够帮到更多的开发者。

长列表界面开发挑战

先来分享一下现在移动开发中遇到的新的挑战,那就是大数据量的列表数据在移动端的展示以及性能,也就是长列表界面在数据量大时可能会遇到的挑战,具体如下所示:

  • 内存消耗:一次性加载所有数据项可能导致内存消耗过大。
  • 加载时间:加载大量数据项需要较长时间,影响用户体验。
  • 滑动流畅度:大量数据项的渲染可能造成界面滑动卡顿。

上述这三点,是我们移动端开发中,必会遇到的问题,这也是我们在实际开发中必须要解决的问题。

关于懒加载

根据上面的挑战,为了解决长列表页面的展示和性能问题,引出来了懒加载技术,其实懒加载是一种按需加载资源的技术,它根据用户的滚动位置动态加载数据项,从而减少初始加载的数据量和渲染压力。这也正是在移动端开发中解决长列表数据展示的最佳解决方案,也是开发者首选的解决方法。

HarmonyOS中的LazyForEach

在HarmonyOS开发中也有懒加载技术,即LazyForEach,其实LazyForEach是从提供的数据源中按需迭代数据,并在每次迭代过程中创建相应的组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会进行组件销毁回收以降低内存占用。

根据鸿蒙官方的显示,LazyForEach在实际应用中也有一定的使用限制,具体有以下几个方面:

  • LazyForEach必须在容器组件内使用,在鸿蒙开发中只有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),而其他组件仍然是一次性加载所有的数据。
  • LazyForEach在每次迭代中,必须创建且只允许创建一个子组件。
  • 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件,不然就不能正常使用。
  • 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
  • 键值生成器必须针对每个数据生成唯一的值,如果键值相同,会导致键值相同的UI组件渲染出现问题。
  • LazyForEach必须使用DataChangeListener对象来进行更新,第一个参数dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
  • 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。

组件的创建

再来分享一下组件创建相关的内容,实际开发中在确定键值生成规则后,LazyForEach的第二个参数itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件,其中LazyForEach组件的创建包括两种情况:首次渲染、非首次渲染。

1、首次渲染

首次渲染会生成不同键值,因为在LazyForEach首次渲染时,会根据上述键值生成规则为数据源的每个数组项生成唯一键值,并创建相应的组件。这里分享一个简单的示例,具体代码如下所示:

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: string[] = [];

  public totalCount(): number {
    return 0;
  }

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

  // LazyForEach组件向其数据源处添加listener监听
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  // 对应的LazyForEach组件在数据源处去除listener监听
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const poss = this.listeners.indexOf(listener);
    if (poss >= 0) {
      this.listeners.splice(poss, 1);
    }
  }

  // 通知LazyForEach组件需要重载所有子组件
  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  // 通知LazyForEach组件需要在index对应索引处添加子组件
  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }

  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  // 通知LazyForEach组件需要在index对应索引处删除该子组件
  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: string[] = [];

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

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

  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();
   
  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.pushData(`sanzhanggui ${i}`)
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(50)
              .onAppear(() => {
                console.info("sanzhanggui:" + item)
              })
          }.margin({ left: 10, right: 10 })
        }
      }, (item: string) => item)
    }.cachedCount(5)
  }
}

通过上面的代码可以看到,键值生成规则是keyGenerator函数的返回值item。在LazyForEach循环渲染时,其为数据源数组项依次生成键值sanzhanggui 0、sanzhanggui 1 ... sanzhanggui 20,并创建对应的ListItem子组件渲染到界面上。

还有一种情况就是键值相同时错误渲染,当不同数据项生成的键值相同时,会出现不可控的效果,比如在LazyForEach渲染的数据项键值均相同,在滑动过程中,LazyForEach会对划入划出当前页面的子组件进行预加载,而新建的子组件和销毁的原子组件具有相同的键值,框架可能存在取用缓存错误的情况,导致子组件渲染有问题。

2、非首次渲染

关于非首次渲染,当LazyForEach数据源发生变化,需要再次渲染时,开发者应根据数据源的变化情况调用listener对应的接口,通知LazyForEach做相应的更新,分为各使用场景:添加数据、删除数据、交换数据、改变单个数据、改变多个数据、精准批量修改数据、改变数据子属性,由于篇幅原因,这里只以添加数据为例来分享,具体如下所示:

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: string[] = [];

  public totalCount(): number {
    return 0;
  }

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

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

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

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
      // 另外一种写法:listener.onDatasetChange([{type: DataOperationType.ADD, index: index}]);
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: string[] = [];

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

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

  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i <= 20; i++) {
      this.data.pushData(`Hello ${i}`)
    }
  }

  build() {
    List({ space: 3 }) {
      LazyForEach(this.data, (item: string) => {
        ListItem() {
          Row() {
            Text(item).fontSize(50)
              .onAppear(() => {
                console.info("sanzhanggui:" + item)
              })
          }.margin({ left: 10, right: 10 })
        }
        .onClick(() => {
          // 追加子组件
          this.data.pushData(`sanzhanggui ${this.data.totalCount()}`);
        })
      }, (item: string) => item)
    }.cachedCount(5)
  }
}

根据上面的代码可以看到,当我们点击LazyForEach的子组件时,首先调用数据源data的pushData方法,该方法会在数据源末尾添加数据并调用notifyDataAdd方法。但是在notifyDataAdd方法内会又调用listener.onDataAdd方法,该方法会通知LazyForEach在该处有数据添加,LazyForEach便会在该索引处新建子组件。

关于长列表拖拽排序

通过上面介绍的关于长列表界面的实际情况使用,这里再分享一个在实际开发中比较常见的需求,那就是根据用户拖拉拽的方式来对长列表数据进行排序。这里用到的技术就是,当LazyForEach在List组件下使用,并且设置了onMove事件,可以使能拖拽排序。拖拽排序离手后,如果数据位置发生变化,则会触发onMove事件,上报数据移动原始索引号和目标索引号。但是在onMove事件中,需要根据上报的起始索引号和目标索引号修改数据源,而且onMove中修改数据源不需要调用DataChangeListener中接口通知数据源变化。下面以一个简单示例来分享:

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: string[] = [];

  public totalCount(): number {
    return 0;
  }

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

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

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

  notifyDataReload(): void {
    this.listeners.forEach(listener => {
      listener.onDataReloaded();
    })
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    })
  }

  notifyDataChange(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataChange(index);
    })
  }

  notifyDataDelete(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataDelete(index);
    })
  }

  notifyDataMove(from: number, to: number): void {
    this.listeners.forEach(listener => {
      listener.onDataMove(from, to);
    })
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: string[] = [];

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

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

  public addData(index: number, data: string): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public moveDataWithoutNotify(from: number, to: number): void {
    let tmp = this.dataArray.splice(from, 1);
    this.dataArray.splice(to, 0, tmp[0])
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  public deleteData(index: number): void {
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index);
  }
}

@Entry
@Component
struct Parent {
  private data: MyDataSource = new MyDataSource();

  build() {
    Row() {
      List() {
        LazyForEach(this.data, (item: string) => {
            ListItem() {
              Text(item.toString())
                .fontSize(16)
                .textAlign(TextAlign.Center)
                .size({height: 120, width: "90%"})
            }.margin(10)
            .borderRadius(10)
            .backgroundColor("#FFFF0000")
          }, (item: string) => item)
          .onMove((from:number, to:number)=>{
            this.data.moveDataWithoutNotify(from, to)
          })
      }
      .width('100%')
      .height('100%')
      .backgroundColor("#4444")
    }
  }
  aboutToAppear(): void {
    for (let i = 0; i < 100; i++) {
      this.data.pushData(i.toString())
    }
  }
}

番外篇:NodeAdapter使用

上面分享的关于LazyForEach的使用,那么鸿蒙官方又针对List、Grid、WaterFlow、Swiper组件,提供NodeAdapter对象替代ArkTS的LazyForEach功能,用于按需生成子组件,其中List组件的属性枚举值为NODE_LIST_NODE_ADAPTER,Grid组件的属性枚举值为NODE_GRID_NODE_ADAPTER,WaterFlow组件的属性枚举值为NODE_WATER_FLOW_NODE_ADAPTER,Swiper组件的属性枚举值为NODE_SWIPER_NODE_ADAPTER。

上面介绍的这两种方式虽然都用于按需生成组件,但不同于ArkTS的LazyForEach,NodeAdapter对象的规格如下:

  • 设置了NodeAdapter属性的节点,不再支持addChild等直接添加子组件的接口。而子组件完全由NodeAdapter管理,使用属性方法设置NodeAdapter时,会判断父组件是否已经存在子节点,如果父组件已经存在子节点,则设置NodeAdapter操作失败,返回错误码。
  • NodeApdater通过相关事件通知开发者按需生成组件,类似组件事件机制,开发者使用NodeAdapter时要注册事件监听器,在监听器事件中处理逻辑,相关事件通过ArkUI_NodeAdapterEventType定义。还有就是NodeAdapter不会主动释放不在屏幕内显示的组件对象,开发者需要在NODE_ADAPTER_EVENT_ON_REMOVE_NODE_FROM_ADAPTER事件中进行组件对象的释放,或者进行缓存复用。

根据鸿蒙官方的介绍,这里分享一下典型列表滑动场景下的事件触发机制,具体如下图所示:

这里关于NodeAdapter的使用只做简单理论分享,具体的使用,待随后做一个专题来分享。

结束语

通过本文的分享,大家对鸿蒙开发中长列表的使用肯定会有更深入的了解,长列表界面是应用中展示大量数据的重要方式,而懒加载技术则是提高长列表性能的关键。在HarmonyOS开发中,通过合理设计适配器、实现懒加载逻辑、异步数据加载和视图复用,可以有效提升长列表的加载速度和滑动流畅度,为用户提供更好的使用体验。我们作为开发者,在实现长列表时应充分考虑性能优化和用户体验,利用HarmonyOS提供的强大API和组件,构建高效、流畅的长列表界面。随着HarmonyOS生态的不断发展壮大,期待更多的创新技术和最佳实践的出现,让我们一起拭目以待吧!

  • 9
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

三掌柜666

如果对您有所帮助,请支持一下呗

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值