HarmonyOS鸿蒙Next中使用ForEach渲染动态列表时,删除中间项后UI错乱

HarmonyOS鸿蒙Next中使用ForEach渲染动态列表时,删除中间项后UI错乱 业务后台返回的列表数据比如为 [A, B, C],删除 B 后 UI 显示为 [A, C, C],末尾项竟然会重复?

8 回复

开发者您好,可采用如下方案解决:

【问题定位】

  1. 找到对应的页面中的ForEach循环,查看itemGenerator与keyGenerator的键值设置。
  2. 查看itemGenerator与keyGenerator逻辑,最终生成的键值规则中是否包含index,或者使用的不是唯一的规则作为键值。

【分析结论】

最终键值生成规则中使用的不是唯一规则作为键值,导致数据源存在不同的键值,创建出了重复的组件。

【修改建议】

保证键值唯一,键值生成规则中,尽量避免不同的索引。

可参考以下代码,此demo采用数据项item作为键值生成规则,由于数据源simpleList的数组项各不相同,因此能够保证键值的唯一性。

@Entry
@Component
struct ArticleList {
  @State simpleList: Array<number> = [1, 2, 3, 4, 5];


  build() {
    Column() {
      ForEach(this.simpleList, (item: number) => {
        ArticleSkeletonView()
          .margin({ top: 20 })
      }, (item: number) => item.toString())
    }
    .padding(20)
    .width('100%')
    .height('100%')
  }
}

【背景知识】

ForEach(arr: Array, itemGenerator: (item: any, index: number) => void, keyGenerator?: (item: any, index: number) => string):基于数组类型数据来进行循环渲染,需要与容器组件配合使用,且接口返回的组件应当是允许包含在ForEach父容器组件中的子组件。

  • arr:数据源,用来渲染UI的数据,可以是任何类型的数组源,比如对象,字符串,数值,都可以。
  • itemGenerator:是组件生成函数,为数组中的每个元素创建对应的组件。
  • keyGenerator:是键值生成函数,为数据源arr的每个数组项生成唯一且持久的键值。

键值生成规则:

  • 当itemGenerator声明index参数,而keyGenerator没有声明index参数时,键值应是keyGenerator函数返回值与index拼接的字符串,当keyGenerator声明index参数时,键值应该是keyGenerator函数返回值。
  • 当itemGenerator没有声明index参数时,keyGenerator函数不管是否声明index,键值都应该是keyGenerator函数返回值。

在实际的渲染过程中,每个数组元素生成一个唯一且持久的键值,用来标记相对应的组件,当键值有变化时,ArkUI框架会认为,当前数组元素替换或修改,会根据新的键值重新创建一个新的组件。

键值的生成规则,直接会影响着数据渲染的UI,因为itemGenerator函数会根据键值生成规则为数据源的每个数组项创建组件。

当不同数组项按照键值生成规则生成的键值相同时,框架认为是未定义的,此时不再创建新的组件。

更多关于HarmonyOS鸿蒙Next中使用ForEach渲染动态列表时,删除中间项后UI错乱的实战系列教程也可以访问 https://www.itying.com/category-93-b0.html


新手常见的问题:

键值生成规则

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

ForEach提供了一个名为keyGenerator的参数,这是一个函数,开发者可以通过它自定义键值的生成规则。如果开发者没有定义keyGenerator函数,则ArkUI框架会使用默认的键值生成函数,即(item: Object, index: number) => { return index + ‘__’ + JSON.stringify(item); }。

ArkUI框架对于ForEach的键值生成有一套特定的判断规则,这主要与itemGenerator函数和keyGenerator函数的第二个参数index有关。具体的键值生成规则判断逻辑如下图所示。

cke_363.png

键值生成示例:

interface ChildItemType {
  str: string;
  num: number;
}
@Entry
@Component
struct Index {
  [@State](/user/State) simpleList: Array<ChildItemType> = [
    { str: 'one', num: 1 },
    { str: 'two', num: 2 },
    { str: 'three', num: 3 }
  ];
  build() {
    Row() {
      Column() {
        ForEach(this.simpleList, (item: ChildItemType, index: number) => {
          ChildItem({ str: item.str, num: index }) // 组件生成函数中使用index参数
        }, (item: ChildItemType, index: number) => {
          return item.str; // 建议在键值生成函数中使用与UI界面相关的数据属性str
        })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}
@Component
struct ChildItem {
  @Prop str: string = '';
  @Prop num: number = 0;
  build() {
    Text(this.str)
      .fontSize(50)
  }
}

在上述示例中,当组件生成函数声明index时,建议键值生成函数也声明index参数,以避免渲染性能降低和渲染结果非预期。同时建议在键值生成函数实现中使用与UI相关的数据属性,在本示例中,数据属性str与UI界面显示相关,因此建议将其作为键值生成函数的返回值。

渲染结果非预期

在本示例中,通过设置ForEach的第三个参数KeyGenerator函数,自定义键值生成规则为数据源的索引index的字符串类型值。当点击父组件ForEachAbnormal中“Insert Item After First Item”文本组件后,界面会出现非预期的结果。

@Entry
@Component
struct ForEachAbnormal {
  [@State](/user/State) simpleList: Array<string> = ['one', 'two', 'three'];
  build() {
    Column() {
      Button() {
        Text('Insert Item After First Item').fontSize(30)
      }
      .onClick(() => {
        this.simpleList.splice(1, 0, 'new item');
      })
      ForEach(this.simpleList, (item: string) => {
        ForEachAbnormalChildItem({ item: item })
      }, (item: string, index: number) => index.toString())
    }
    .justifyContent(FlexAlign.Center)
    .width('100%')
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}
@Component
struct ForEachAbnormalChildItem {
  @Prop item: string;
  build() {
    Text(this.item)
      .fontSize(30)
  }
}

cke_3183.gif

ForEach在首次渲染时,创建的键值依次为"0"、“1”、“2”。

插入新项后,数据源simpleList变为[‘one’, ‘new item’, ‘two’, ‘three’],框架监听到@State装饰的数据源长度变化触发ForEach重新渲染。

ForEach依次遍历新数据源,遍历数据项"one"时生成键值"0",存在相同键值,因此不创建新组件。继续遍历数据项"new item"时生成键值"1",存在相同键值,因此不创建新组件。继续遍历数据项"two"生成键值"2",存在相同键值,因此不创建新组件。最后遍历数据项"three"时生成键值"3",不存在相同键值,创建内容为"three"的新组件并渲染。

从以上可以看出,当键值包含数据项索引index时,期望的界面渲染结果为[‘one’, ‘new item’, ‘two’, ‘three’],而实际的渲染结果为[‘one’, ‘two’, ‘three’, ‘three’],不符合开发者预期。因此,开发者在使用ForEach时应避免键值包含索引index。

一般是唯一的key不对引起的,先使用

JSON.stringify()

最唯一的key试试。删除数据之后数据源的的数据记得也删掉。

如果数据量很大不建议使用JSON.stringify()作为唯一的key

ForEach(arr: Array<any>, itemGenerator: (item: any, index: number) => void, keyGenerator?: (item: any, index: number) => string)

ForEach接口基于数组类型数据来进行循环渲染。

第三个参数 keyGenerator 键值生成函数,为数据源arr的每个数组项生成唯一且持久的键值。

如果渲染结果非预期,建议开发者自定义键值,使用对象数据中的唯一id作为键值。

interface ChildItemType {
  str: string;
  num: number;
}

@Entry
@Component
struct Index {
  @State simpleList: Array<ChildItemType> = [
    { str: 'one', num: 1 },
    { str: 'two', num: 2 },
    { str: 'three', num: 3 }
  ];

  build() {
    Row() {
      Column() {
        ForEach(this.simpleList, (item: ChildItemType, index: number) => {
          ChildItem({ str: item.str, num: index }) // 组件生成函数中使用index参数
        }, (item: ChildItemType, index: number) => {
          return item.str; // 建议在键值生成函数中使用与UI界面相关的数据属性str
        })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

@Component
struct ChildItem {
  @Prop str: string = '';
  @Prop num: number = 0;

  build() {
    Text(this.str)
      .fontSize(50)
  }
}

是不是因为 key 不唯一或不稳定 导致的复用错误。必须确保itemGeneratoritemKey 使用唯一且不变的标识(如数据库 ID),而非数组索引。

看代码如何写的,测试下示例,看删除是正常:

interface ChildItemType {
  str: string;
  num: number;
}

@Entry
@Component
struct Index {
  @State simpleList: Array<ChildItemType> = [
    { str: 'A', num: 1 },
    { str: 'B', num: 2 },
    { str: 'C', num: 3 }
  ];

  build() {
    Column() {
        Column() {
          ForEach(this.simpleList, (item: ChildItemType, index: number) => {
            ChildItem({ str: item.str, num: index }) // 组件生成函数中使用index参数
          }, (item: ChildItemType, index: number) => {
            return item.str; // 建议在键值生成函数中使用与UI界面相关的数据属性str
          })
        }
        .width('100%')
        .height(200)

      Text("delete B")
        .width('100%')
        .height(30)
        .onClick(()=>{
          this.simpleList.splice(1, 1);
        })
        .backgroundColor(Color.Red)
    }
    .height('100%')
    .backgroundColor(0xF1F3F5)
  }
}

@Component
struct ChildItem {
  @Prop str: string = '';
  @Prop num: number = 0;

  build() {
    Text(this.str)
      .fontSize(50)
  }
}

在HarmonyOS Next中,使用ForEach渲染动态列表时,删除中间项导致UI错乱,通常是因为列表项的唯一标识符(key)未正确设置或管理。ForEach依赖key来识别和跟踪列表项的变化,如果key不唯一或未随数据更新,删除操作可能引发渲染错误。确保每个列表项都有稳定且唯一的key,并避免使用索引作为key,特别是在动态增删场景下。

在HarmonyOS Next中,ForEach渲染动态列表时出现删除中间项后UI错乱(如末尾项重复),核心原因是列表项的键(key)未正确设置或管理,导致ArkUI框架在差异更新(Diff)时无法准确识别和追踪每个列表项的身份。

问题根源分析: ForEach 根据数据源和键生成组件。当数据从 [A, B, C] 变为 [A, C] 时,框架会进行差异比较。如果未提供有效的键或键不唯一/不稳定,框架可能错误地认为第二个数据项 C 对应的是旧的第三个项 C,而原本的第二个项 B 被移除。这会导致组件实例复用错乱,状态(如文本、样式)未能正确更新,从而出现显示为 [A, C, C] 的异常。

解决方案:

  1. 为数据项设置唯一且稳定的键:确保每个数据对象都有一个唯一标识(如 id 字段),并在 ForEachkeyGenerator 参数中返回该值。

    // 假设数据项有唯一id字段
    ForEach(
      this.itemList,
      (item: YourItemType) => {
        // item.id 作为唯一键
      },
      (item: YourItemType) => item.id.toString() // keyGenerator:返回唯一键
    )
    

    如果数据源是数组且项本身无唯一标识,避免使用数组索引作为键,因为索引会随数据顺序变化,不稳定。若必须用索引,需确保数据顺序变化与UI更新逻辑严格匹配。

  2. 使用@State@Observed装饰器管理数据源:确保数据源是响应式的。当数据变更(如删除B)时,ArkUI能检测到状态变化并触发UI更新。

    @State itemList: YourItemType[] = [A, B, C];
    // 删除操作应直接修改this.itemList,例如使用splice
    
  3. 检查数据更新逻辑:确保删除操作是直接对响应式数据源进行的不可变更新(例如使用splice或展开运算符生成新数组),而不是直接修改数组内容或索引映射。

总结:此问题通常由键管理不当引起。请为ForEach提供唯一键,并确保数据源响应式更新,即可避免删除中间项时的UI错乱。

回到顶部