HarmonyOS鸿蒙Next中使用ForEach渲染动态列表时,删除中间项后UI错乱
HarmonyOS鸿蒙Next中使用ForEach渲染动态列表时,删除中间项后UI错乱
业务后台返回的列表数据比如为 [A, B, C],删除 B 后 UI 显示为 [A, C, C],末尾项竟然会重复?
开发者您好,可采用如下方案解决:
【问题定位】
- 找到对应的页面中的ForEach循环,查看itemGenerator与keyGenerator的键值设置。
- 查看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有关。具体的键值生成规则判断逻辑如下图所示。

键值生成示例:
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)
}
}

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


