HarmonyOS鸿蒙Next中ArkTS使用LazyForEach渲染长列表时,滚动到末尾后新加载的数据未触发UI更新,如何修复?

HarmonyOS鸿蒙Next中ArkTS使用LazyForEach渲染长列表时,滚动到末尾后新加载的数据未触发UI更新,如何修复? 实现分页加载列表,首次渲染 20 条数据正常。滚动到底部触发加载下一页,数据已追加到 @State items: Array<Item>,但 UI 未刷新,需手动下拉才显示,如何修复?

5 回复

开发者您好,通过如下demo并未复现您的问题

示例代码如下:

export class WaterFlowDataSource implements IDataSource {
  private dataArray: number[] = [];
  private listeners: DataChangeListener[] = [];

  constructor() {
    for (let i = 0; i < 20; i++) {
      this.dataArray.push(i);
    }
  }

  // 获取索引对应的数据
  public getData(index: number): number {
    return this.dataArray[index];
  }

  // 通知控制器数据重新加载
  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);
    });
  }

  // 获取数据总数
  public totalCount(): number {
    return this.dataArray.length;
  }

  // 注册改变数据的控制器
  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  // 注销改变数据的控制器
  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  // 增加数据
  public add1stItem(): void {
    this.dataArray.splice(0, 0, this.dataArray.length);
    this.notifyDataAdd(0);
  }

  // 在数据尾部增加一个元素
  public addLastItem(): void {
    this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
    this.notifyDataAdd(this.dataArray.length - 1);
  }

  // 在指定索引位置增加一个元素
  public addItem(index: number): void {
    this.dataArray.splice(index, 0, this.dataArray.length);
    this.notifyDataAdd(index);
  }

  // 删除第一个元素
  public delete1stItem(): void {
    this.dataArray.splice(0, 1);
    this.notifyDataDelete(0);
  }

  // 删除第二个元素
  public delete2ndItem(): void {
    this.dataArray.splice(1, 1);
    this.notifyDataDelete(1);
  }

  // 删除最后一个元素
  public deleteLastItem(): void {
    this.dataArray.splice(-1, 1);
    this.notifyDataDelete(this.dataArray.length);
  }

  // 在指定索引位置删除一个元素
  public deleteItem(index: number): void {
    this.dataArray.splice(index, 1);
    this.notifyDataDelete(index);
  }

  // 重新加载数据
  public reload(): void {
    this.dataArray.splice(1, 1);
    this.dataArray.splice(3, 2);
    this.notifyDataReload();
  }
}
import { WaterFlowDataSource } from './DataSource';

@Entry
@Component
struct WaterFlowDemo {
  private minSize: number = 80;
  private maxSize: number = 180;
  @State colors: number[] = [0x86C5E3, 0x61CFBE, 0x8981F7, 0x86C5E3, 0x61CFBE];
  scroller: Scroller = new Scroller();
  dataSource: WaterFlowDataSource = new WaterFlowDataSource();
  private itemWidthArray: number[] = [];

  // 设置FlowItem的宽/高数组
  setItemSizeArray() {
    for (let i = 0; i < 20; i++) {
      if (i === 0) {
        this.itemWidthArray.push(this.minSize);
      }
      this.itemWidthArray.push(this.maxSize);
    }
  }

  aboutToAppear() {
    this.setItemSizeArray();
  }

  build() {
    Column({ space: 2 }) {
      WaterFlow() {
        LazyForEach(this.dataSource, (item: number) => {
          FlowItem() {
            Column() {
              Text(`N${item}`).fontSize(12).height('16')
            }
          }
          .width(this.itemWidthArray[item % 100])
          .height('30%')
          .backgroundColor(this.colors[item % 3])
        }, (item: string) => item)
      }
      .layoutDirection(FlexDirection.Column)
      .columnsTemplate('1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr')
      .columnsGap(10)
      .rowsGap(5)
      .backgroundColor(0xFAEEE0)
      .width('100%')
      .height('100%')
      // 触底加载数据
      .onReachEnd(() => {
        console.info('onReachEnd');
        for (let i = 0; i < 20; i++) {
          this.dataSource.addLastItem();
        }
      })
    }
  }
}

LazyForEach依赖唯一键值来标识组件。不管是原来的数据数组中key值不唯一还是增删修改数组后key值不唯一,都会导致组件渲染异常。异常的表现在item缺失,重复等。如果设置了key值还是不能解决您的问题,麻烦您提供下能复现问题的完整demo吧。

更多关于HarmonyOS鸿蒙Next中ArkTS使用LazyForEach渲染长列表时,滚动到末尾后新加载的数据未触发UI更新,如何修复?的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


ForEach/LazyForEach 刷新原理:如果未提供 keyGenerator,框架会基于 item 和 index 自动生成 key。默认的键值生成函数为 (item: T, index: number) => index + '__' + JSON.stringify(item)。修改状态变量数据源时,ForEach 或 LazyForEach 会捕捉到 key 的变化,从而通过重建组件节点来刷新。

参考链接:https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-rendering-control-lazyforeach

小伙伴你好,简单分析了一下问题原因可能出现在:

@State 的监听限制

[@State](/user/State) 装饰器仅能感知数组第一层引用变化(如整体数组替换)若直接修改原数组(如 push() 追加元素),数组引用未变,系统无法触发 UI 更新。

能否提供一下相关代码段或示例 Demo 吗?

在HarmonyOS鸿蒙Next中,使用LazyForEach渲染长列表时,滚动到末尾后新加载数据未触发UI更新,通常是由于数据源未正确通知UI层数据变更所致。

确保数据源类继承自DataSource,并在数据更新后调用notifyDataReload()notifyDataChange()方法。检查LazyForEachdataSource参数是否绑定到该可观察对象。

若使用数组作为数据源,需将其包装为@Observed类中的属性,并在更新数组后,通过重新赋值整个数组或使用this.dataArray.splice()等操作触发UI刷新。直接使用push等方法可能不会触发更新。

在HarmonyOS Next中,LazyForEach与@State结合使用时,数据源更新后UI未自动刷新的问题,通常是由于数据引用未发生改变导致ArkUI框架未感知到状态变化。

核心原因@State装饰的数组,当通过push、splice等方法直接修改其内容时,数组的引用地址未变,LazyForEach可能无法正确触发重新渲染。

解决方案

  1. 为数组分配新引用(推荐): 在追加新数据时,不要直接修改原数组,而是创建一个全新的数组。

    // 错误方式:直接修改原数组
    // this.items.push(...newItems);
    
    // 正确方式:创建新数组
    this.items = this.items.concat(newItems);
    // 或使用扩展运算符
    this.items = [...this.items, ...newItems];
    
  2. 使用@Link或复杂状态管理: 如果数据源来自父组件,考虑使用@Link装饰器,或在子组件中使用@ObjectLink、@Observed配合自定义类,确保嵌套属性的变化能被观察到。

  3. 检查LazyForEach的键值生成: 确保keyGenerator函数返回唯一且稳定的键值。新增数据的键值不能与已有数据重复,否则可能影响渲染。

    LazyForEach(
      this.items,
      (item: Item) => item.id.toString(), // 确保id唯一
      (item: Item) => {
        // 列表项组件
      }
    )
    
  4. 强制刷新(临时方案): 在极少数情况下,可以尝试通过改变数组引用来强制刷新(但应先尝试上述方法)。

    // 先解构再赋值
    this.items = [...this.items];
    

实现示例

[@State](/user/State) items: Array<Item> = [];

loadNextPage() {
  // 模拟获取新数据
  const newItems: Item[] = [...];
  // 正确更新:创建新数组
  this.items = this.items.concat(newItems);
}

确保在滚动到底部触发加载的函数中,以上述方式更新items数组。这样LazyForEach会检测到数据源引用变化,并正确更新UI。

回到顶部