HarmonyOS NEXT Practical: List and LazyForeach
Goal: Implement list layout and load item items through lazy loading. Prerequisite: ohos.permission.INTERNET Permission application required Implementation idea: Create a Product Model model Create BasicDataSource data source Integrate BasicDataSource and customized ListDataSource Implement LazyForEach loop on the page LazyForEach usage restrictions LazyForEach must be used within the container component, and only the List, Grid, Swiper, and WaterFlow components support lazy data loading (with the configurable cached count property, which only loads the visible portion and a small amount of data before and after it for buffering), while other components still load all data at once. LazyForEach depends on the generated key value to determine whether to refresh the sub component. If the key value does not change, LazyForEach cannot be triggered to refresh the corresponding sub component. When using LazyForEach within a container component, only one LazyForEach can be included. Taking List as an example, it is not recommended to include ListItem, ForEach, and LazyForEach simultaneously; It is also not recommended to include multiple LazyForEach simultaneously. LazyForEach must create and only allows the creation of one sub component in each iteration; The sub component generation function of LazyForEach has only one root component. The generated child components must be allowed to be included in the LazyForEach parent container component. Allow LazyForEach to be included in if/else conditional rendering statements, and also allow if/else conditional rendering statements to appear in LazyForEach. The key value generator must generate unique values for each data, and if the key values are the same, it will cause rendering problems for UI components with the same key values. LazyForEach must be updated using the DataChangeListener object, and reassigning the first parameter dataSource will result in an exception; When dataSource uses state variables, changes in the state variables will not trigger a UI refresh for LazyForEach. For high-performance rendering, when updating the UI through the onDataChange method of the DataChangeListener object, it is necessary to generate different key values to trigger component refresh. LazyForEach must be used with the @Reusable decorator to trigger node reuse. Usage: Decorate @Reusable on the component of LazyForEach list, see usage rules. ProductModel export interface ProductModel{ engineerId:string, engineerName:string, mobile:string, avatarImg:string, storeId:string, storeName:string, engineerLevel:string, orderNumber:string, } BasicDataSource export class BasicDataSource implements IDataSource { private listeners: DataChangeListener[] = []; private originDataArray: T[] = []; public totalCount(): number { return 0; } public getData(index: number): T { return this.originDataArray[index]; } registerDataChangeListener(listener: DataChangeListener): void { if (this.listeners.indexOf(listener) < 0) { console.info('add listener'); this.listeners.push(listener); } } unregisterDataChangeListener(listener: DataChangeListener): void { const pos = this.listeners.indexOf(listener); if (pos >= 0) { console.info('remove listener'); this.listeners.splice(pos, 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); }) } notifyDatasetChange(operations: DataOperation[]): void { this.listeners.forEach(listener => { listener.onDatasetChange(operations); }) } } ListDataSource import { BasicDataSource } from "./BasicDataSource"; import { ProductModel } from "./ProductModel"; export class ListDataSource extends BasicDataSource { private dataArray: ProductModel[] = []; public totalCount(): number { return this.dataArray.length; } public getData(index: number): ProductModel { return this.dataArray[index]; } getAllData():ProductModel[] | null{ return this.dataArray } public pushData(data: ProductModel): void { this.dataArray.push(data); this.notifyDataAdd(this.dataArray.length - 1); } } ListDemoPage import { router } from '@kit.ArkUI'; import { ListDataSource } from './ListDataSource'; import { ProductModel } from './ProductModel'; @Entry @Component struct ListDemoPage { @StorageProp('bottomRectHeight') bottomRectHeight: numb

Goal: Implement list layout and load item items through lazy loading.
Prerequisite: ohos.permission.INTERNET Permission application required
Implementation idea:
- Create a Product Model model
- Create BasicDataSource data source
- Integrate BasicDataSource and customized ListDataSource
- Implement LazyForEach loop on the page
LazyForEach usage restrictions
- LazyForEach must be used within the container component, and only the List, Grid, Swiper, and WaterFlow components support lazy data loading (with the configurable cached count property, which only loads the visible portion and a small amount of data before and after it for buffering), while other components still load all data at once.
- LazyForEach depends on the generated key value to determine whether to refresh the sub component. If the key value does not change, LazyForEach cannot be triggered to refresh the corresponding sub component.
- When using LazyForEach within a container component, only one LazyForEach can be included. Taking List as an example, it is not recommended to include ListItem, ForEach, and LazyForEach simultaneously; It is also not recommended to include multiple LazyForEach simultaneously.
- LazyForEach must create and only allows the creation of one sub component in each iteration; The sub component generation function of LazyForEach has only one root component.
- The generated child components must be allowed to be included in the LazyForEach parent container component.
- Allow LazyForEach to be included in if/else conditional rendering statements, and also allow if/else conditional rendering statements to appear in LazyForEach.
- The key value generator must generate unique values for each data, and if the key values are the same, it will cause rendering problems for UI components with the same key values.
- LazyForEach must be updated using the DataChangeListener object, and reassigning the first parameter dataSource will result in an exception; When dataSource uses state variables, changes in the state variables will not trigger a UI refresh for LazyForEach.
- For high-performance rendering, when updating the UI through the onDataChange method of the DataChangeListener object, it is necessary to generate different key values to trigger component refresh.
- LazyForEach must be used with the @Reusable decorator to trigger node reuse. Usage: Decorate @Reusable on the component of LazyForEach list, see usage rules.
ProductModel
export interface ProductModel{
engineerId:string,
engineerName:string,
mobile:string,
avatarImg:string,
storeId:string,
storeName:string,
engineerLevel:string,
orderNumber:string,
}
BasicDataSource
export class BasicDataSource implements IDataSource {
private listeners: DataChangeListener[] = [];
private originDataArray: T[] = [];
public totalCount(): number {
return 0;
}
public getData(index: number): T {
return this.originDataArray[index];
}
registerDataChangeListener(listener: DataChangeListener): void {
if (this.listeners.indexOf(listener) < 0) {
console.info('add listener');
this.listeners.push(listener);
}
}
unregisterDataChangeListener(listener: DataChangeListener): void {
const pos = this.listeners.indexOf(listener);
if (pos >= 0) {
console.info('remove listener');
this.listeners.splice(pos, 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);
})
}
notifyDatasetChange(operations: DataOperation[]): void {
this.listeners.forEach(listener => {
listener.onDatasetChange(operations);
})
}
}
ListDataSource
import { BasicDataSource } from "./BasicDataSource";
import { ProductModel } from "./ProductModel";
export class ListDataSource extends BasicDataSource {
private dataArray: ProductModel[] = [];
public totalCount(): number {
return this.dataArray.length;
}
public getData(index: number): ProductModel {
return this.dataArray[index];
}
getAllData():ProductModel[] | null{
return this.dataArray
}
public pushData(data: ProductModel): void {
this.dataArray.push(data);
this.notifyDataAdd(this.dataArray.length - 1);
}
}
ListDemoPage
import { router } from '@kit.ArkUI';
import { ListDataSource } from './ListDataSource';
import { ProductModel } from './ProductModel';
@Entry
@Component
struct ListDemoPage {
@StorageProp('bottomRectHeight')
bottomRectHeight: number = 0;
@StorageProp('topRectHeight')
topRectHeight: number = 0;
@State currentPageNum: number = 1
total: number = 0
private data: ListDataSource = new ListDataSource();
isLoading: boolean = false;
@State loadSuccess: boolean = true
async aboutToAppear(): Promise {
await this.initData()
}
async initData() {
this.isLoading = true;
await this.listProduct()
this.isLoading = false;
}
async listProduct() {
const param: Param = {
"data": { "storeId": 331, "cityId": 320100 },
"pageNum": this.currentPageNum,
"pageSize": 10
}
//填入模拟数据
this.total = 20;
for (let i = 0; i <= 20; i++) {
this.data.pushData({
engineerId: i.toString(),
engineerName: '小白' + (Math.floor(Math.random() * 100) + 1),
mobile: '12341234' + i,
avatarImg: 'https://oss.cloudhubei.com.cn/cms/release/set35/20241014/f3af08b621af0b7c0648c48dcd964000.jpg',
storeId: 'storeId' + i,
storeName: 'storeName' + i,
engineerLevel: '1',
orderNumber: i.toString(),
})
}
}
build() {
Column({ space: 10 }) {
this.header()
this.content()
}
.width('100%')
.height('100%')
.padding({ top: this.topRectHeight })
}
@Builder
header() {
Row() {
Row({ space: 20 }) {
Image($r('app.media.icon_back'))
.width(18)
.height(12)
.responseRegion([{
x: -9,
y: -6,
width: 36,
height: 24
}])
Text('Beauty List')
.fontWeight(700)
.fontColor('#525F7F')
.fontSize(16)
.lineHeight(22)
}
Row({ space: 6 }) {
SymbolGlyph($r('sys.symbol.clean_fill'))
.fontSize(18)
.renderingStrategy(SymbolRenderingStrategy.SINGLE)
.fontColor([Color.Black])
Text('清除本地缓存')
.fontSize(14)
.fontColor(Color.Black)
}
.onClick(() => {
router.replaceUrl({ url: 'pages/BeautyListPage' })
})
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 20, right: 20 })
}
@Builder
content() {
List() {
LazyForEach(this.data, (item: ProductModel) => {
ListItem() {
Row({ space: 10 }) {
this.buildImage(item.avatarImg != '' ? item.avatarImg : 'https://oss.cloudhubei.com.cn/cms/release/set35/20241014/f3af08b621af0b7c0648c48dcd964000.jpg')
Column() {
Text(item.engineerName)
}
.layoutWeight(1)
}
.width('100%')
.height(100)
}
.borderRadius(4)
.clip(true)
.backgroundColor(Color.White)
.margin({ right: 20, left: 20, top: 10 })
}, (item: string) => item)
ListItem().height(this.bottomRectHeight)
}
.width('100%')
.backgroundColor('#F8F9FE')
.layoutWeight(1)
.cachedCount(15)
.scrollBar(BarState.Off)
.onReachEnd(async () => {
if (!this.isLoading) {
this.isLoading = true;
this.currentPageNum++
await this.listProduct()
this.isLoading = false;
}
})
}
@Builder
buildImage(src:string){
Row() {
if (this.loadSuccess) {
Image(src)
.width('100%')
.height('100%')
.onError(() => {
this.loadSuccess = false
})
} else {
Text('图片加载失败...').margin(10).fontColor(Color.Gray)
}
}
.width('50%')
.height(100)
.backgroundColor('#eeeeee')
.justifyContent(FlexAlign.Center)
}
}
interface Param {
"data": Record;
"pageNum": number;
"pageSize": number;
}