HarmonyOS 鸿蒙Next中大量数据全量加载卡顿,后续加载导致checkboxgroup选择异常

HarmonyOS 鸿蒙Next中大量数据全量加载卡顿,后续加载导致checkboxgroup选择异常 现在有5000条json对象数据(数量不可控可能更多),按照50一组分组(每组点击可折叠或展示)。

利用foreach全量加载页面会极其卡顿甚至闪退,一进页面立马点击全选,随着后续加载该选择状态会异常甚至每次点击不能立即反应

使用懒加载也会卡顿(感觉卡顿比foreach更严重,foreach只是首次全量加载卡顿,懒加载全程都在卡),同时懒加载的组件回收机制也会导致全选状态异常

请问该如何实现顺畅的全量加载并且不影响全选状态?

(由于checkboxgroup不能多层嵌套,我是讲每一个小分组包一个checkgroup,最大的全选状态通过观察每个小分组的全选状态进行遍历来确定)


更多关于HarmonyOS 鸿蒙Next中大量数据全量加载卡顿,后续加载导致checkboxgroup选择异常的实战教程也可以访问 https://www.itying.com/category-93-b0.html

10 回复

尊敬的开发者,您好,

长列表作为应用开发中最常见的开发场景之一,通常会包含成千上万个列表项,在此场景下,直接使用循环渲染ForEach一次性加载所有的列表项,会导致渲染时间过长,影响用户体验。而使用数据懒加载LazyForEach替换循环渲染ForEach,可以按需加载列表项,从而提升列表性能。数据懒加载的示例代码可以参考LazyForEach

虽然,按需加载列表项可以优化长列表性能,但在快速滑动长列表的场景下,可能会来不及加载需要显示的列表项,导致出现白块的现象,从而影响用户体验。而在ArkUI中,List容器提供了cachedCount属性,LazyForEach可以结合cachedCount属性一起使用,能够避免白块的现象。cachedCount可以设置列表中ListItem/ListItemGroup的预加载数量,并且只在LazyForEach中生效,即cachedCount只能与LazyForEach一起使用。除了List容器,其他容器Grid、Swiper以及WaterFlow也都包含cachedCount属性。cachedCount的使用方法如下所示。

List() {
  // ...
}.cachedCount(3)

此外,HarmonyOS应用框架提供了组件复用能力,可以结合LazyForEach一起使用,进一步优化长列表的性能。组件复用会把组件树上将要移除的组件进行回收,回收的组件会进入到一个回收缓存区。后续创建新组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。关于组件复用的详细原理可以参考组件复用。针对长列表加载的性能优化,可以参考优化长列表加载慢丢帧问题

示例代码如下:

class BasicDataSource implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: Array<TimeTable> | Array<CheckboxItemData> = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): TimeTable | CheckboxItemData {
    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);
    })
  }
}

class MyDataSource extends BasicDataSource {
  private dataArray: TimeTable[] | Array<CheckboxItemData> = [];

  constructor(data: Array<TimeTable> | Array<CheckboxItemData>) {
    super();
    this.dataArray = data;
  }

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): TimeTable | CheckboxItemData {
    return this.dataArray[index];
  }

  public addDataTimeTable(index: number, data: TimeTable): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public addDataCheckboxItemData(index: number, data: CheckboxItemData): void {
    this.dataArray.splice(index, 0, data);
    this.notifyDataAdd(index);
  }

  public pushDataTimeTable(data: TimeTable): void {
    (this.dataArray as Array<TimeTable>).push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  public pushDataCheckboxItemData(data: CheckboxItemData): void {
    (this.dataArray as Array<CheckboxItemData>).push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  public operateCheckboxItemData(isSelect: boolean): void {
    (this.dataArray as Array<CheckboxItemData>).forEach((item) => {
      item.isCheck = isSelect
    })

    this.notifyDataReload()
  }

  public operateCheckboxItem(isSelect: boolean, index: number): void {
    (this.dataArray as Array<CheckboxItemData>)[index].isCheck = isSelect
    this.notifyDataChange(index)
  }

  public getDataSource(): CheckboxItemData[] {
    return this.dataArray as CheckboxItemData[]
  }
}

/*
 * The course schedule data structure, title represents the day of the week, and projects represents the list of courses for that day
 */
@Observed
class CheckboxItemData{
  isCheck: boolean = false;
  itemName: string = '';
  constructor(name:string) {
    this.itemName = name
  }
}
@Observed
class TimeTable {
  title: string = ''
  data: CheckboxItemData[] = [];
  constructor(til:string) {
    this.title = til
  }
}

@Entry
@Component
export struct Index {
  @State timeTable: TimeTable[] = [];
  private data1: MyDataSource = new MyDataSource([]);

  @Provide isSelectAllAll:boolean = false

  aboutToAppear(): void {
    for (let index = 0; index < 1000; index++) {
      this.timeTable.push(new TimeTable(`第${index+1}组`))
      for(let j = 0;j<50;j++){
        this.timeTable[index].data.push(new CheckboxItemData(`checkbox-${index+1}-${j}`))
      }
      this.data1.pushDataTimeTable(this.timeTable[index])
    }
  }

  build() {
    Column(){
      Button(this.isSelectAllAll?'全不选':'全选')
        .onClick(()=>{
          this.isSelectAllAll = !this.isSelectAllAll
        })
      List({ space: 3 }) {
        // ForEach(this.timeTable,(item: TimeTable)=>{
        //     ListItem() {
        //       Text(item.title)
        //         .width('100%')
        //         .height(100)
        //         .fontSize(20)
        //         .textAlign(TextAlign.Center)
        //         .backgroundColor(0xFFFFFF)
        //     }
        // },(item: TimeTable) => item.title)
        LazyForEach(this.data1, (item: TimeTable) => {
          ListItem() {
            CheckGroupComp({data:item})
          }
        }, (item: TimeTable) => item.title)
      }
      .cachedCount(5)
      .width('100%')
      .height('100%')
    }
  }
}

@Component
struct CheckGroupComp{
  @ObjectLink data:TimeTable
  private data2: MyDataSource = new MyDataSource([]);
  @State isSelectAll: boolean = false

  @Watch('selectAll') @Consume isSelectAllAll:boolean

  selectAll(){
    this.data2.operateCheckboxItemData(this.isSelectAllAll)
    this.isSelectAll = this.isSelectAllAll
  }

  aboutToAppear(): void {
    if(this.data){
      this.data.data.forEach(it=>{
        this.data2.pushDataCheckboxItemData(it)
      })
    }
  }
  build() {
    Column() {
      Flex({ justifyContent: FlexAlign.Start, alignItems: ItemAlign.Center }) {
        CheckboxGroup({ group: this.data.title+"group" })
          .selectAll(this.isSelectAll)
          .hitTestBehavior(HitTestMode.None)
        Text(`${this.data.title}全选`).fontSize(25).padding(12)
      }.onClick(() => {
        this.isSelectAll = !this.isSelectAll
        this.data2.operateCheckboxItemData(this.isSelectAll)
      }).padding({ left: 10 })

      List({ space: 3 }) {
        LazyForEach(this.data2, (item: CheckboxItemData, index: number) => {
          ListItem() {
            Row() {
              Checkbox({ name: `checkbox-${item}` })
                .select(item.isCheck)
                .onChange((value: boolean) => {
                  this.data2.operateCheckboxItem(value, index)
                  let dataSource = this.data2.getDataSource()
                  this.isSelectAll = dataSource.every((item) => item.isCheck === true)
                })
              Text(item.itemName).fontSize(20)
            }.margin({ left: 10, right: 10 })
          }

        }, (item: CheckboxItemData) => item.itemName + item.isCheck)
      }
      .height(300)
      .cachedCount(5)
    }
  }
}

如果还是不能解决您的问题,麻烦您提供下能复现问题的最小demo吧。

更多关于HarmonyOS 鸿蒙Next中大量数据全量加载卡顿,后续加载导致checkboxgroup选择异常的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


你这是明显的包含复选框的树型结构,可以试试OHPM仓库的第三方组件:
https://ohpm.openharmony.cn/#/cn/result?sortedType=relevancy&page=1&q=tree,

不能用外部三方库和ComponentV2

方便提供下demo看看嘛?

找HarmonyOS工作还需要会Flutter的哦,有需要Flutter教程的可以学学大地老师的教程,很不错,B站免费学的哦:https://www.bilibili.com/video/BV1S4411E7LY/?p=17

不一定采用CheckBoxGroup的方式去做可以使用list的方式自己模拟

毋庸置疑采用懒加载,是否可以提供ui图看看,懒加载是否成功您这边有测试过吗?

懒加载有数据,就是上下滑动会非常卡顿

HarmonyOS 鸿蒙Next大量数据全量加载卡顿是因一次性创建所有组件导致主线程阻塞,应使用LazyForEach实现按需加载。checkboxgroup选择异常通常源于数据源更新后未正确维护选中状态,或列表回收时checkbox实例状态丢失,需确保数据源与选择状态绑定并随列表刷新正确同步。

针对大数据量分组列表卡顿和全选状态异常,核心解决思路是:只渲染可见内容 + 集中管理选中状态

方案简述

  • 数据展示:列表使用 List + LazyForEach,仅对当前展开的分组渲染其下50条数据;折叠的分组不渲染子项,仅渲染分组头。这样实际屏幕上仅需加载极少量的 Item 节点,大幅减少首次渲染和滚动压力。
  • 选中状态管理:用一个大数组(或 Map)selectedMap 集中存储每条数据的勾选状态(例如 index 或 id 为 key)。每个分组的 CheckboxGroup 通过 @Watch 监听该分组的子项选择变化,直接修改 selectedMap;全选则是遍历所有子项批量修改selectedMap,驱动 UI 更新。
  • 避免组件回收丢失状态LazyForEach 回收组件时,通过数据源的方法 onMoveToCache 或直接忽略,因为勾选状态已保存在 selectedMap 中,再次渲染时会重新读取正确状态。

关键代码片段

// 全局选中状态 Map
@State selectedMap: Map<number, boolean> = new Map();

// 数据源模型
class DataItem {
  id: number;
  name: string;
  // ...
}

// 分组模型
class Group {
  title: string;
  items: DataItem[];
  isExpanded: boolean = false;
}

// 构建 List
List() {
  LazyForEach(this.dataSource, (group: Group, groupIndex: number) => {
    ListItem() {
      Column() {
        // 分组头,点击展开/折叠
        Text(group.title)
          .onClick(() => {
            group.isExpanded = !group.isExpanded;
            this.dataSource.notifyDataChange(groupIndex); // 刷新视图
          })
        // 仅展开时渲染子项
        if (group.isExpanded) {
          ForEach(group.items, (item: DataItem, itemIndex: number) => {
            Checkbox({ name: `item_${groupIndex}_${itemIndex}`, group: `group_${groupIndex}` })
              .select(this.selectedMap.get(item.id) ?? false)
              .onChange((value: boolean) => {
                this.selectedMap.set(item.id, value);
                // 可在此处检查分组全选状态并更新分组 Checkbox 标识
              })
          })
        }
      }
    }
  })
}

核心要点

  1. 只渲染当前展开的分组,未展开的分组只占一行,5000 条数据最多渲染一两个分组(不到 100 个 item),全程流畅。
  2. 勾选状态与视图解耦,状态存在 selectedMap 中,不受组件回收影响;展开新分组时重新读取 selectedMap 显示正确状态。
  3. 全选操作:直接遍历所有需要选中的 item id,写入 selectedMap,然后通知对应可见分组刷新即可,无性能压力。

采用此方案,5000 条乃至更多数据都可丝滑操作,全选状态也不会因组件回收而错乱。

回到顶部