您的位置:首页 > 游戏 > 手游 > HarmonyOS开发:长列表界面实现详解(使用懒加载)

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

2024/12/23 6:47:30 来源:https://blog.csdn.net/CC1991_/article/details/141273110  浏览:    关键词: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生态的不断发展壮大,期待更多的创新技术和最佳实践的出现,让我们一起拭目以待吧!

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com