##HarmonyOS Next实战##HarmonyOS应用开发##教育##
目标:实现列表布局,并且通过懒加载加载item项。
前提:需要申请权限ohos.permission.INTERNET。
实现思路:
- 创建ProductModel模型
- 创建BasicDataSource数据源
- 集成BasicDataSource和定制化ListDataSource
- 在页面实现LazyForEach循环
LazyForEach使用限制
- LazyForEach必须在容器组件内使用,仅有List、Grid、Swiper以及WaterFlow组件支持数据懒加载(可配置cachedCount属性,即只加载可视部分以及其前后少量数据用于缓冲),其他组件仍然是一次性加载所有的数据。
- LazyForEach依赖生成的键值判断是否刷新子组件,若键值不发生改变,则无法触发LazyForEach刷新对应的子组件。
- 容器组件内使用LazyForEach的时候,只能包含一个LazyForEach。以List为例,同时包含ListItem、ForEach、LazyForEach的情形是不推荐的;同时包含多个LazyForEach也是不推荐的。
- LazyForEach在每次迭代中,必须创建且只允许创建一个子组件;即LazyForEach的子组件生成函数有且只有一个根组件。
- 生成的子组件必须是允许包含在LazyForEach父容器组件中的子组件。
- 允许LazyForEach包含在if/else条件渲染语句中,也允许LazyForEach中出现if/else条件渲染语句。
- 键值生成器必须针对每个数据生成唯一的值,如果键值相同,将导致键值相同的UI组件渲染出现问题。
- LazyForEach必须使用DataChangeListener对象进行更新,对第一个参数dataSource重新赋值会异常;dataSource使用状态变量时,状态变量改变不会触发LazyForEach的UI刷新。
- 为了高性能渲染,通过DataChangeListener对象的onDataChange方法来更新UI时,需要生成不同于原来的键值来触发组件刷新。
- LazyForEach必须和@Reusable装饰器一起使用才能触发节点复用。使用方法:将@Reusable装饰在LazyForEach列表的组件上,见使用规则。
ProductModel
export interface ProductModel{
engineerId:string,
engineerName:string,
mobile:string,
avatarImg:string,
storeId:string,
storeName:string,
engineerLevel:string,
orderNumber:string,
}
BasicDataSource
export class BasicDataSource<T> 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<ProductModel> {
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<void> {
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<string, number>;
"pageNum": number;
"pageSize": number;
}