HarmonyOS NEXT实战:列表和懒加载

3 阅读3分钟

##HarmonyOS Next实战##HarmonyOS应用开发##教育##

目标:实现列表布局,并且通过懒加载加载item项。

前提:需要申请权限ohos.permission.INTERNET。

实现思路:

  1. 创建ProductModel模型
  2. 创建BasicDataSource数据源
  3. 集成BasicDataSource和定制化ListDataSource
  4. 在页面实现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;
}