目录
- 前言
- 长列表界面开发挑战
- 关于懒加载
- 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生态的不断发展壮大,期待更多的创新技术和最佳实践的出现,让我们一起拭目以待吧!