在鴻蒙Next開發(fā)中,F(xiàn)orEach接口用于循環(huán)渲染數(shù)組類型數(shù)據(jù),與容器組件配合使用,可高效構(gòu)建動態(tài)列表等UI元素。以下是ForEach用法的詳細(xì)總結(jié)。
一、鍵值生成規(guī)則
-
系統(tǒng)默認(rèn)規(guī)則:若開發(fā)者未定義keyGenerator函數(shù),ArkUI框架使用默認(rèn)函數(shù)
(item: Object, index: number) => { return index + '__' + JSON.stringify(item); }生成鍵值。 - 自定義規(guī)則:通過提供keyGenerator函數(shù)來自定義鍵值生成邏輯。
- 警告與限制:框架會對重復(fù)鍵值發(fā)出警告,重復(fù)鍵值可能導(dǎo)致UI更新異常。例如,當(dāng)不同數(shù)組項按規(guī)則生成相同鍵值時,行為可能不符合預(yù)期。
二、組件創(chuàng)建規(guī)則
1. 首次渲染
- 根據(jù)鍵值生成規(guī)則為數(shù)據(jù)源每個數(shù)組項生成唯一鍵值,并創(chuàng)建相應(yīng)組件。
- 示例:
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
ForEach(this.simpleList, (item: string ) => {
ChildItem({ item: item })
}, (item: string) => item)
}
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(50)
}
}
- 上述代碼中,鍵值生成規(guī)則為
item,為數(shù)據(jù)源數(shù)組項依次生成鍵值one、two和three,并創(chuàng)建對應(yīng)的ChildItem組件渲染到界面。
2. 非首次渲染
- 檢查新生成鍵值是否在上次渲染中已存在。若不存在,則創(chuàng)建新組件;若存在,則復(fù)用對應(yīng)組件。
- 示例:
@Entry
@Component
struct Parent {
@State simpleList: Array<string> = ['one', 'two', 'three'];
build() {
Row() {
Column() {
Text('點擊修改第3個數(shù)組項的值')
.fontSize(24)
.fontColor(Color.Red)
.onClick(() => {
this.simpleList[2] = 'new three';
})
ForEach(this.simpleList, (item: string ) => {
ChildItem({ item: item })
.margin({ top: 20 })
}, (item: string) => item)
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
}
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ChildItem {
@Prop item: string;
build() {
Text(this.item)
.fontSize(30)
}
}
- 點擊修改數(shù)組項值后,F(xiàn)orEach遍歷新數(shù)據(jù)源
['one', 'two', 'new three'],鍵值one和two已存在,復(fù)用對應(yīng)組件,而new three鍵值不存在,創(chuàng)建新組件。
三、使用場景
1. 數(shù)據(jù)源不變
- 數(shù)據(jù)源可直接采用基本數(shù)據(jù)類型,如使用骨架屏列表渲染展示頁面加載狀態(tài)。
- 示例:
@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%')
}
}
@Builder
function textArea(width: number | Resource | string = '100%', height: number | Resource | string = '100%') {
Row()
.width(width)
.height(height)
.backgroundColor('#FFF2F3F4')
}
@Component
struct ArticleSkeletonView {
build() {
Row() {
Column() {
textArea(80, 80)
}
.margin({ right: 20 })
Column() {
textArea('60%', 20)
textArea('50%', 20)
}
.alignItems(HorizontalAlign.Start)
.justifyContent(FlexAlign.SpaceAround)
.height('100%')
}
.padding(20)
.borderRadius(12)
.backgroundColor('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
2. 數(shù)據(jù)源數(shù)組項發(fā)生變化
- 如進(jìn)行數(shù)組插入、刪除操作或數(shù)組項索引交換,數(shù)據(jù)源應(yīng)為對象數(shù)組類型,使用對象唯一ID作為最終鍵值。
- 示例:
class Article {
id: string;
title: string;
brief: string;
constructor(id: string, title: string, brief: string) {
this.id = id;
this.title = title;
this.brief = brief;
}
}
@Entry
@Component
struct ArticleListView {
@State isListReachEnd: boolean = false;
@State articleList: Array<Article> = [
new Article('001', '第1篇文章', '文章簡介內(nèi)容'),
new Article('002', '第2篇文章', '文章簡介內(nèi)容'),
new Article('003', '第3篇文章', '文章簡介內(nèi)容'),
new Article('004', '第4篇文章', '文章簡介內(nèi)容'),
new Article('005', '第5篇文章', '文章簡介內(nèi)容'),
new Article('006', '第6篇文章', '文章簡介內(nèi)容')
];
loadMoreArticles() {
this.articleList.push(new Article('007', '加載的新文章', '文章簡介內(nèi)容'));
}
build() {
Column({ space: 5 }) {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
ArticleCard({ article: item })
.margin({ top: 20 })
}
}, (item: Article) => item.id)
}
.onReachEnd(() => {
this.isListReachEnd = true;
})
.parallelGesture(
PanGesture({ direction: PanDirection.Up, distance: 80 })
.onActionStart(() => {
if (this.isListReachEnd) {
this.loadMoreArticles();
this.isListReachEnd = false;
}
})
)
.padding(20)
.scrollBar(BarState.Off)
}
.width('100%')
.height('100%')
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ArticleCard {
@Prop article: Article;
build() {
Row() {
Image($r('app.media.icon'))
.width(80)
.height(80)
.margin({ right: 20 })
Column() {
Text(this.article.title)
.fontSize(20)
.margin({ bottom: 8 })
Text(this.article.brief)
.fontSize(16)
.fontColor(Color.Gray)
.margin({ bottom: 8 })
}
.alignItems(HorizontalAlign.Start)
.width('80%')
.height('100%')
}
.padding(20)
.borderRadius(12)
.backgroundColor('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
3. 數(shù)據(jù)源數(shù)組項子屬性變化
- 當(dāng)數(shù)據(jù)源為對象數(shù)組且僅修改數(shù)組項屬性值時,需結(jié)合
@Observed和@ObjectLink裝飾器使用,以使ForEach重新渲染。 - 示例:
@Observed
class Article {
id: string;
title: string;
brief: string;
isLiked: boolean;
likesCount: number;
constructor(id: string, title: string, brief: string, isLiked: boolean, likesCount: number ) {
this.id = id;
this.title = title;
this.brief = brief;
this.isLiked = isLiked;
this.likesCount = likesCount;
}
}
@Entry
@Component
struct ArticleListView {
@State articleList: Array<Article> = [
new Article('001', '第0篇文章', '文章簡介內(nèi)容', false, 100),
new Article('002', '第1篇文章', '文章簡介內(nèi)容', false, 100),
new Article('003', '第2篇文章', '文章簡介內(nèi)容', false, 100),
new Article('004', '第4篇文章', '文章簡介內(nèi)容', false, 100),
new Article('005', '第5篇文章', '文章簡介內(nèi)容', false, 100),
new Article('006', '第6篇文章', '文章簡介內(nèi)容', false, 100),
];
build() {
List() {
ForEach(this.articleList, (item: Article) => {
ListItem() {
ArticleCard({
article: item
})
.margin({ top: 20 })
}
}, (item: Article) => item.id)
}
.padding(20)
.scrollBar(BarState.Off)
.backgroundColor(0xF1F3F5)
}
}
@Component
struct ArticleCard {
@ObjectLink article: Article;
handleLiked() {
this.article.isLiked =!this.article.isLiked;
this.article.likesCount = this.article.isLiked? this.article.likesCount + 1 : this.article.likesCount - 1;
}
build() {
Row() {
Image($r('app.media.icon'))
.width(80)
.height(80)
.margin({ right: 20 })
Column() {
Text(this.article.title)
.fontSize(20)
.margin({ bottom: 8 })
Text(this.article.brief)
.fontSize(16)
.fontColor(Color.Gray)
.margin({ bottom: 8 })
Row() {
Image(this.article.isLiked? $r('app.media.iconLiked') : $r('app.media.iconUnLiked'))
.width(24)
.height(24)
.margin({ right: 8 })
Text(this.article.likesCount.toString())
.fontSize(16)
}
.onClick(() => this.handleLiked())
.justifyContent(FlexAlign.Center)
}
.alignItems(HorizontalAlign.Start)
.width('80%')
.height('100%')
}
.padding(20)
.borderRadius(12)
.backgroundColor('#FFECECEC')
.height(120)
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
}
}
4. 拖拽排序
- 當(dāng)ForEach在
List組件下使用且設(shè)置onMove事件,每次迭代生成ListItem時,可實現(xiàn)拖拽排序。數(shù)據(jù)源修改前后要保持?jǐn)?shù)據(jù)鍵值不變,僅順序變化,以保證落位動畫正常執(zhí)行。 - 示例:
@Entry
@Component
struct ForEachSort {
@State arr: Array<string> = [];
build() {
Row() {
List() {
ForEach(this.arr, (item: string ) => {
ListItem() {
Text(item.toString())
.fontSize(16)
.textAlign(TextAlign.Center)
.size({height: 100, width: "100%"})
}.margin(10)
.borderRadius(10)
.backgroundColor("#FFFFFFFF")
}, (item: string) => item)
.onMove((from:number, to:number) => {
let tmp = this.arr.splice(from, 1);
this.arr.splice(to, 0, tmp[0])
})
}
.width('100%')
.height('100%')
.backgroundColor("#FFDCDCDC")
}
}
aboutToAppear(): void {
for (let i = 0; i < 100; i++) {
this.arr.push(i.toString())
}
}
}
四、使用建議
-
鍵值選擇:對于對象數(shù)據(jù)類型,建議使用對象唯一ID作為鍵值。避免在最終鍵值生成規(guī)則中包含數(shù)據(jù)項索引
index,除非業(yè)務(wù)必需,因包含index可能導(dǎo)致渲染結(jié)果非預(yù)期和性能降低。 - 數(shù)據(jù)類型轉(zhuǎn)換:基本數(shù)據(jù)類型數(shù)組在數(shù)據(jù)源會變化的場景下,建議轉(zhuǎn)換為具備唯一ID屬性的對象數(shù)據(jù)類型數(shù)組,并使用ID屬性作為鍵值生成規(guī)則。
-
容器組件使用限制:ForEach在
List、Grid、Swiper、WaterFlow等容器組件內(nèi)使用時,不要與LazyForEach混用。
五、常見問題
1. 渲染結(jié)果非預(yù)期
- 若最終鍵值生成規(guī)則包含
index,可能出現(xiàn)渲染結(jié)果不符合預(yù)期的情況。如在特定示例中,插入新項后渲染結(jié)果與期望不符。
2. 渲染性能降低
- 若使用框架默認(rèn)鍵值生成規(guī)則(包含
index),在數(shù)據(jù)源變化時可能導(dǎo)致組件大量重新創(chuàng)建,影響性能。例如,插入新數(shù)組項時,后面所有數(shù)組項對應(yīng)的組件可能都需重新創(chuàng)建,當(dāng)數(shù)據(jù)量較大或組件結(jié)構(gòu)復(fù)雜時,性能體驗不佳。
掌握ForEach的用法和相關(guān)注意事項,有助于在鴻蒙Next開發(fā)中高效構(gòu)建動態(tài)UI,提升應(yīng)用性能和用戶體驗。