前言导读
各位同学大家,有段时间没有跟大家见面了,因为最近一直在更新鸿蒙的那个实战课程所以就没有去更新文章实在是不好意思, 昨晚引文有网友问到鸿蒙next 里面的数据懒加载 我这边就做一个分享
效果图
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必须和@Reusable装饰器一起使用才能触发节点复用。使用方法:将@Reusable装饰在LazyForEach列表的组件上,见使用规则。
键值生成规则
在LazyForEach循环渲染过程中,系统会为每个item生成一个唯一且持久的键值,用于标识对应的组件。当这个键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并会基于新的键值创建一个新的组件。
LazyForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return viewId + '-' + index.toString(); }, viewId在编译器转换过程中生成,同一个LazyForEach组件内其viewId是一致的。
组件创建规则
在确定键值生成规则后,LazyForEach的第二个参数itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件。组件的创建包括两种情况:LazyForEach首次渲染和LazyForEach非首次渲染。
具体实现 :
-
本地数据模拟实现懒加载
// Basic implementation of IDataSource to handle data listener
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) {
console.info('add listener');
this.listeners.push(listener);
}
}
// 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 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 Index {
private data: MyDataSource = new MyDataSource();
aboutToAppear() {
for (let i = 0; i <= 1000; i++) {
this.data.pushData(`掏粪男孩 ${i}`)
}
}
build() {
List({ space: 3 }) {
LazyForEach(this.data, (item: string) => {
ListItem() {
ChildComponent({getdata: item})
}
}, (item: string) => item)
}.cachedCount(5)
}
}
@Reusable
@Component
struct ChildComponent {
getdata!: string
build() {
Row() {
Text(this.getdata).fontSize(50)
.onAppear(() => {
console.info("appear:" + this.getdata)
})
}.margin({ left: 10, right: 10 })
}
}
```
网络请求的数据懒加载
-
处理数据list item的服用
import Logger from '../utils/Logger';
import { httpRequestGet } from '../utils/OKhttpUtil';
import CommonConstant, * as commonConst from '../common/CommonConstants';
import { Positiondata, PositionModel } from '../bean/PositionModel';
class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: Array<Positiondata> = [];
public totalCount(): number {
return 0;
}
public getData(index: number): Positiondata {
return this.originDataArray[index];
}
// 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}
// 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 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: Array<Positiondata> = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): Positiondata {
return this.dataArray[index];
}
public addData(index: number, data:Positiondata): void {
this.dataArray.splice(index, 0, data);
this.notifyDataAdd(index);
}
public pushData(data: Positiondata): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
@Entry
@Component
struct Index {
private JokeListdata: MyDataSource = new MyDataSource();
@State TAG: string = 'Companylist --- > '
@State positiondata: Positiondata = new Positiondata()
@State JokeList: Array<Positiondata> = []
async aboutToAppear(){
Logger.error(this.TAG+' aboutToAppear --- > ');
let networkurl=CommonConstant.DISH;
await httpRequestGet(networkurl).then((data)=>{
console.log(this.TAG +"data --- > "+data);
Logger.error(this.TAG+"登录请求回调结果 ---> " +data.toString());
let positionmodel : PositionModel = JSON.parse(data.toString())
this.JokeList = positionmodel.data
for (let i = 0; i <= this.JokeList.length; i++) {
this.JokeListdata.pushData(this.JokeList[i]);
}
});
}
build() {
List({ space: 3 }) {
LazyForEach(this.JokeListdata, (item: Positiondata,index :number) => {
ListItem() {
ChildComponent({getdata:item})
}
}, (item: string) => item)
}.cachedCount(5)
}
}
@Reusable
@Component
struct ChildComponent {
getdata!: Positiondata
build() {
Row() {
Column() {
Row() {
Text(this.getdata?.name).fontSize(14).fontColor($r('app.color.gray')) .margin({left:20})
Text(this.getdata?.salary).fontSize(20).fontColor($r('app.color.freshRed'))
.margin({left:140})
.align(Alignment.BottomStart)
}.justifyContent(FlexAlign.Center)
.width(commonConst.GOODS_LIST_WIDTH)
Text(this.getdata?.cname)
.fontSize(25)
.margin({ left: 10 })
Divider().width('90%').backgroundColor(Color.Black)
Text(this.getdata?.username)
.fontColor($r('app.color.greentext'))
.fontSize(12)
.margin({ left:10, top: 10 })
}
//.padding(commonConst.GOODS_LIST_PADDING)
.width(commonConst.LAYOUT_WIDTH_OR_HEIGHT)
.height(commonConst.LAYOUT_WIDTH_OR_HEIGHT)
.justifyContent(FlexAlign.Start)
}
.justifyContent(FlexAlign.Center)
.height(commonConst.GOODS_LIST_HEIGHT)
.width(commonConst.LAYOUT_WIDTH_OR_HEIGHT)
}
}
-
数据model
export class PositionModel { msg: string = "" data: Array<Positiondata> = []; code:number=0; } export class Positiondata { id: string = ""; name: string = ""; cname: string = ""; size: string = ""; salary: string = ""; username: string = ""; title: string = ""; }
-
布局代码 懒加载 LazyForEach
build() { List({ space: 3 }) { LazyForEach(this.JokeListdata, (item: Positiondata,index :number) => { ListItem() { Row() { Column() { Row() { Text(item?.name).fontSize(14).fontColor($r('app.color.gray')) .margin({left:20}) Text(item?.salary).fontSize(20).fontColor($r('app.color.freshRed')) .margin({left:140}) .align(Alignment.BottomStart) }.justifyContent(FlexAlign.Center) .width(commonConst.GOODS_LIST_WIDTH) Text(item?.cname) .fontSize(25) .margin({ left: 10 }) Divider().width('90%').backgroundColor(Color.Black) Text(item?.username) .fontColor($r('app.color.greentext')) .fontSize(12) .margin({ left:10, top: 10 }) } //.padding(commonConst.GOODS_LIST_PADDING) .width(commonConst.LAYOUT_WIDTH_OR_HEIGHT) .height(commonConst.LAYOUT_WIDTH_OR_HEIGHT) .justifyContent(FlexAlign.Start) } .justifyContent(FlexAlign.Center) .height(commonConst.GOODS_LIST_HEIGHT) .width(commonConst.LAYOUT_WIDTH_OR_HEIGHT) } }, (item: string) => item) }.cachedCount(5) }
使用懒加载开发长列表界面
针对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事件中进行组件对象的释放,或者进行缓存复用。下图展示了典型列表滑动场景下的事件触发机制:
最后总结
今天分享的list 组建里面的 LazyForEach 懒加载类似我们安卓里面adapter 和ios里面自定义的cell 就是我们滑动页面的时候加载新的item的时候系统不再重新创建对象 而是回收我们已经使用过item的对象 这样我们就可以复用item 就可以在加载 长 列表的时候减少开销从而提升滑动的性能和流畅度。 因为篇幅有限我也不能整个项目都展开讲,有兴趣的同学能可以关注我B站课程。 后续能我会把这个项目更新到项目里面 供大家学习
B站课程地址:www.bilibili.com/cheese/play…
团队介绍
团队介绍:坚果派由坚果等人创建,团队由12位华为HDE以及若干热爱鸿蒙的开发者和其他领域的三十余位万粉博主运营。专注于分享 HarmonyOS/OpenHarmony,ArkUI-X,元服务,仓颉,团队成员聚集在北京,上海,南京,深圳,广州,宁夏等地,目前已开发鸿蒙 原生应用,三方库60+,欢迎进行课程,项目等合作。