管理中心
我的
您当前正在浏览新版开发者文档中心,目录分类和层级有所调整。点击左侧当前文档分类名称前的“☰”图标,可切换文档分类。 了解新版目录
问题处理与运维开发与测试问题应用框架开发UI框架UI界面应用内列表滑动后出现重复数据

应用内列表滑动后出现重复数据

问题现象

进入应用,在应用搜索框输入搜索关键词,得到搜索结果数据列表,进行滑动之后,出现了重复的搜索结果。

背景知识

LazyForEach:数据懒加载,LazyForEach从数据源中按需迭代数据,并在每次迭代时创建相应组件。当在滚动容器中使用了LazyForEach,框架会根据滚动容器可视区域按需创建组件,当组件滑出可视区域外时,框架会销毁并回收组件以降低内存占用。

在LazyForEach循环渲染过程中,系统为每个item生成一个唯一且持久的键值,用于标识对应的组件。键值变化时,ArkUI框架将视为该数组元素已被替换或修改,并基于新的键值创建新的组件。

问题定位

  1. 复现问题获取日志,从问题日志信息中可以看到,应用在数据懒加载的过程中,使用了重复的键值:
    收起
    自动换行
    深色代码主题
    复制
    1. 06-13 14:52:33.803 30081-30081 C0392D/com.dou...ceLazyForEach com.xxx.xxx W [(100000:100000:scope)] Use repeat key for index: 3
    2. 06-13 14:52:33.804 30081-30081 C0392D/com.dou...ceLazyForEach com.xxx.xxx W [(100000:100000:scope)] Use repeat key for index: 4
    3. 06-13 14:52:33.805 30081-30081 C0392D/com.dou...ceLazyForEach com.xxx.xxx W [(100000:100000:scope)] Use repeat key for index: 13
    4. 06-13 14:52:33.806 30081-30081 C0392D/com.dou...ceLazyForEach com.xxx.xxx W [(100000:100000:scope)] Use repeat key for index: 14
    5. 06-13 14:52:33.807 30081-30081 C0392D/com.dou...ceLazyForEach com.xxx.xxx W [(100000:100000:scope)] Use repeat key for index: 15
  2. 进一步分析在数据懒加载的过程中,当不同数据项生成的键值相同时,框架的行为是不可预测的。在滑动过程中,LazyForEach会预加载划入划出当前页面的子组件,而新建的子组件和销毁的旧子组件具有相同的键值,框架可能取用错误的缓存,导致子组件渲染出现问题。
  3. 若未发现键值重复日志或问题,可能是数据源重复导致的,需要排查数据源是否重复。

分析结论

应用在数据懒加载的过程中,使用了重复的键值,导致子组件渲染出现问题。

修改建议

修改LazyForEach的键值生成函数,使每个数据项生成唯一的键值,保证渲染效果符合预期。示例代码如下:

收起
自动换行
深色代码主题
复制
  1. @Entry
  2. @Component
  3. struct LazyForEachTest {
  4. private arr: ListDataSource = new ListDataSource([0, 1, 2, 3, 4, 5, 6, 7, 8, 9]);
  5. build() {
  6. Column() {
  7. List({ space: 20, initialIndex: 0 }) {
  8. LazyForEach(this.arr, (item: number) => {
  9. ListItem() {
  10. Text(item.toString())
  11. .width('100%')
  12. .height(100)
  13. .fontSize(16)
  14. .textAlign(TextAlign.Center)
  15. .borderRadius(10)
  16. .backgroundColor(0xFFFFFF)
  17. }
  18. }, (item: string) => item)
  19. }
  20. .cachedCount(5)
  21. .listDirection(Axis.Vertical) // 排列方向
  22. .scrollBar(BarState.Off)
  23. .friction(0.6)
  24. .divider({
  25. strokeWidth: 2,
  26. color: 0xFFFFFF,
  27. startMargin: 20,
  28. endMargin: 20
  29. }) // 每行之间的分界线
  30. .edgeEffect(EdgeEffect.Spring) // 边缘效果设置为Spring
  31. .width('90%')
  32. }
  33. .width('100%')
  34. .height('100%')
  35. .backgroundColor(0xDCDCDC)
  36. .padding({ top: 5 })
  37. }
  38. }

以下是数据项中自定义数据源的实现:

收起
自动换行
深色代码主题
复制
  1. class ListDataSource implements IDataSource {
  2. private list: number[] = [];
  3. private listeners: DataChangeListener[] = [];
  4. constructor(list: number[]) {
  5. this.list = list;
  6. }
  7. totalCount(): number {
  8. return this.list.length;
  9. }
  10. getData(index: number): number {
  11. return this.list[index];
  12. }
  13. registerDataChangeListener(listener: DataChangeListener): void {
  14. if (this.listeners.indexOf(listener) < 0) {
  15. this.listeners.push(listener);
  16. }
  17. }
  18. unregisterDataChangeListener(listener: DataChangeListener): void {
  19. const pos = this.listeners.indexOf(listener);
  20. if (pos >= 0) {
  21. this.listeners.splice(pos, 1);
  22. }
  23. }
  24. // 通知控制器数据删除
  25. notifyDataDelete(index: number): void {
  26. this.listeners.forEach(listener => {
  27. listener.onDataDelete(index);
  28. });
  29. }
  30. // 通知控制器添加数据
  31. notifyDataAdd(index: number): void {
  32. this.listeners.forEach(listener => {
  33. listener.onDataAdd(index);
  34. });
  35. }
  36. // 在指定索引位置删除一个元素
  37. public deleteItem(index: number): void {
  38. this.list.splice(index, 1);
  39. this.notifyDataDelete(index);
  40. }
  41. // 在指定索引位置插入一个元素
  42. public insertItem(index: number, data: number): void {
  43. this.list.splice(index, 0, data);
  44. this.notifyDataAdd(index);
  45. }
  46. }