• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Repeat:可复用的循环渲染
2
3> **说明:**
4>
5> Repeat从API version 12开始支持。
6>
7> 本文档仅为开发者指南。API参数说明见:[Repeat API参数说明](../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md)。
8
9## 概述
10
11Repeat基于数组类型数据来进行循环渲染,一般与容器组件配合使用。Repeat组件包含两种模式:**non-virtualScroll模式**和**virtualScroll模式**。
12
13- **non-virtualScroll模式**:Repeat在初始化页面时加载列表中的所有子组件,适合**短数据列表/组件全部加载**的场景。详细描述见[non-virtualScroll模式](#non-virtualscroll模式)。
14- **virtualScroll模式**(开启[virtualScroll](../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#virtualscroll)开关):Repeat根据容器组件的**有效加载范围(可视区域+预加载区域)** 加载子组件。当容器滑动/数组改变时,Repeat会根据父容器组件传递的参数重新计算有效加载范围,实时管理列表节点的创建与销毁。
15<br/>该模式适合**需要懒加载的长数据列表/通过组件复用优化性能表现**的场景。详细描述见[virtualScroll模式](#virtualscroll模式)。
16
17> **说明:**
18>
19> Repeat与ForEach、LazyForEach的区别:
20>
21> - 相较于[ForEach](arkts-rendering-control-foreach.md)组件,non-virtualScroll模式在以下两个维度实现了优化升级:首先,针对特定数组更新场景的渲染性能进行了优化;其次,将子组件的内容/索引管理职责转移至框架层面。
22> - 相较于[LazyForEach](arkts-rendering-control-lazyforeach.md)组件,virtualScroll模式直接监听状态变量的变化,而LazyForEach需要开发者实现[IDataSource](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#idatasource10)接口,手动管理子组件内容/索引的修改。除此之外,Repeat还增强了节点复用能力,提高了长列表滑动和数据更新的渲染性能。Repeat增加了模板(template)的能力,在同一个数组中,根据开发者自定义的模板类型(template type)渲染不同的子组件。
23
24下面的示例代码使用Repeat组件的virtualScroll模式进行循环渲染。
25
26```ts
27// 在List容器组件中使用Repeat virtualScroll模式
28@Entry
29@ComponentV2 // 推荐使用V2装饰器
30struct RepeatExample {
31  @Local dataArr: Array<string> = []; // 数据源
32
33  aboutToAppear(): void {
34    for (let i = 0; i < 50; i++) {
35      this.dataArr.push(`data_${i}`); // 为数组添加一些数据
36    }
37  }
38
39  build() {
40    Column() {
41      List() {
42        Repeat<string>(this.dataArr)
43          .each((ri: RepeatItem<string>) => { // 默认模板
44            ListItem() {
45              Text('each_A_' + ri.item).fontSize(30).fontColor(Color.Red) // 文本颜色为红色
46            }
47          })
48          .key((item: string, index: number): string => item) // 键值生成函数
49          .virtualScroll({ totalCount: this.dataArr.length }) // 打开virtualScroll模式,totalCount为期望加载的数据长度
50          .templateId((item: string, index: number): string => { // 根据返回值寻找对应的模板子组件进行渲染
51            return index <= 4 ? 'A' : (index <= 10 ? 'B' : ''); // 前5个节点模板为A,接下来的5个为B,其余为默认模板
52          })
53          .template('A', (ri: RepeatItem<string>) => { // 'A'模板
54            ListItem() {
55              Text('ttype_A_' + ri.item).fontSize(30).fontColor(Color.Green) // 文本颜色为绿色
56            }
57          }, { cachedCount: 3 }) // 'A'模板的缓存列表容量为3
58          .template('B', (ri: RepeatItem<string>) => { // 'B'模板
59            ListItem() {
60              Text('ttype_B_' + ri.item).fontSize(30).fontColor(Color.Blue) // 文本颜色为蓝色
61            }
62          }, { cachedCount: 4 }) // 'B'模板的缓存列表容量为4
63      }
64      .cachedCount(2) // 容器组件的预加载区域大小
65      .height('70%')
66      .border({ width: 1 }) // 边框
67    }
68  }
69}
70```
71
72运行后界面如下图所示。
73
74![Repeat-NonVS-KeyGen](./figures/Repeat-Example.png)
75
76## 使用限制
77
78- Repeat一般与容器组件配合使用,子组件应当是允许包含在容器组件中的子组件,例如,Repeat与[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)组件配合使用时,子组件必须为[ListItem](../reference/apis-arkui/arkui-ts/ts-container-listitem.md)组件。
79- 当Repeat与自定义组件或[@Builder函数](./arkts-builder.md)混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化,如果只传递`RepeatItem.item`或`RepeatItem.index`,将会出现UI渲染异常。详细见[Repeat与@Builder混用的限制](#repeat与builder混用的限制)。
80
81Repeat virtualScroll模式使用限制:
82
83- 必须在滚动类容器组件内使用,仅有[List](../reference/apis-arkui/arkui-ts/ts-container-list.md)、[Grid](../reference/apis-arkui/arkui-ts/ts-container-grid.md)、[Swiper](../reference/apis-arkui/arkui-ts/ts-container-swiper.md)以及[WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md)组件支持Repeat virtualScroll模式。
84- virtualScroll模式不支持V1装饰器,混用V1装饰器会导致渲染异常,不建议开发者同时使用。
85- 必须创建且只允许创建一个子组件,生成的子组件必须是允许包含在Repeat父容器组件中的子组件。
86- 滚动容器组件内只能包含一个Repeat。以List为例,同时包含ListItem、ForEach、LazyForEach的场景是不推荐的;同时包含多个Repeat也是不推荐的。
87- totalCount值大于数组长度时,在父组件容器滚动过程中,应用需要保证列表即将滑动到数据源末尾时请求后续数据,直到数据源全部加载完成,否则列表滑动的过程中会出现滚动效果异常。解决方案见[totalCount值大于数据源长度](#totalcount值大于数据源长度)。
88
89Repeat通过键值识别数组如何改变:增加了哪些数据、删除了哪些数据,以及哪些数据改变了位置(索引)。键值生成函数`.key()`的使用建议如下:
90
91- 即使数组发生变化,开发者也必须保证键值key唯一。
92- 每次执行`.key()`函数时,使用相同的数据项作为输入,输出必须是一致的。
93- 允许在`.key()`中使用index,但不建议开发者这样做。因为在数据项移动时索引index发生变化,key值因此改变,导致Repeat认为数据发生了变化,从而触发子组件重新渲染,降低性能表现。
94- 推荐将简单类型数组转换为类对象数组,并添加一个`readonly id`属性,在构造函数中给它赋一个唯一的值。
95
96> **说明:**
97>
98> Repeat子组件节点的操作分为四种:节点创建、节点更新、节点复用、节点销毁。其中,节点更新和节点复用的区别为:
99>
100> - **节点更新**:组件节点不下树,只有状态变量刷新。
101> - **节点复用**:旧的节点下树,但不会销毁,存储在空闲节点缓存池;新节点从缓存池中获取可复用的节点,重新上树。
102
103## non-virtualScroll模式
104
105### 键值生成规则
106
107`.key()`的逻辑如下图所示。
108
109当`.key()`缺省时,Repeat会生成新的随机键值。当发现有重复key时,Repeat会在已有键值的基础上递归生成新的键值,直到没有重复键值。
110
111![Repeat-NonVS-KeyGen](./figures/Repeat-NonVS-KeyGen.png)
112
113### 子组件渲染逻辑
114
115在Repeat首次渲染时,子组件全部创建。数组发生改变后,Repeat的处理分为以下几个步骤:
116
117首先,遍历旧数组键值,如果新数组中没有该键值,将其加入键值集合deletedKeys。
118
119其次,遍历新数组键值,依次判断以下条件,符合条件时进行对应的操作:
120
1211. 若在旧数组中能找到相同键值,直接使用对应的子组件节点,并更新索引index;
1222. 若deletedKeys非空,按照先进后出的顺序,更新该集合中的键值所对应的节点;
1233. 若deletedKeys为空,则表示没有可以更新的节点,需要创建新节点。
124
125最后,如果新数组键值遍历结束后,deletedKeys非空,则销毁集合中的键值所对应的节点。
126
127![Repeat-NonVS-FuncGen](./figures/Repeat-NonVS-FuncGen.png)
128
129以下图中的数组变化为例。
130
131![Repeat-NonVS-Example](./figures/Repeat-NonVS-Example.png)
132
133根据上述判断逻辑,`item_0`没有变化,`item_1`和`item_2`只更新了索引,`item_n1`和`item_n2`分别由`item_4`和`item_3`进行节点更新获得,`item_n3`为新创建的节点。
134
135## virtualScroll模式
136
137### 键值生成规则
138
139和non-virtualScroll模式的逻辑基本一致,如下图所示。
140
141当`.key()`缺省时,Repeat会生成新的随机键值。当存在重复key时,Repeat会重新生成随机key作为当前数据项的键值并且放进该列表。列表中已有的键值不受影响。随机key的构成:`___${index}_+_${key}_+_${Math.random()}`,其中的变量依次为:索引、旧键值、随机数。
142
143![Repeat-VS-KeyGen](./figures/Repeat-VS-KeyGen.png)
144
145### 子组件渲染逻辑
146
147在Repeat首次渲染时,根据容器组件的有效加载范围(可视区域+预加载区域)创建当前需要的子组件。
148
149在容器滑动/数组改变时,将失效的子组件节点(离开有效加载范围)加入空闲节点缓存列表中(断开与组件树的关系,但不销毁),在需要生成新的组件时,对缓存里的组件进行复用(更新被复用子组件的变量值,重新上树)。
150
151Repeat组件在virtualScroll模式下默认启用复用功能。从API version 16开始,可以通过配置`reusable`字段选择是否启用复用功能。为提高渲染性能,建议启用复用功能。代码示例见[VirtualScrollOptions对象说明](../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#virtualscrolloptions对象说明)。
152
153#### 滑动场景
154
155滑动前节点现状如下图所示
156
157![Repeat-Start](./figures/Repeat-Start.PNG)
158
159当前Repeat组件template type有a和b两种,template type等于a对应的缓存池,其最大缓存值为3,template type等于b对应的缓存池,其最大缓存值为4,其父组件默认预加载节点1个。这时,向右滑动屏幕(屏幕内容右移),Repeat开始复用缓存池中的节点。
160
161![Repeat-Slide](./figures/Repeat-Slide.PNG)
162
163index=18的数据进入屏幕及父组件预加载的范围内,此时计算出其template type等于b,这时Repeat会从template type等于b的缓存池中取出一个节点进行复用,更新它的key&index&data,该子节点内部使用了该项数据及索引的其他孙子节点会根据V2状态管理的规则做同步更新。
164
165index=10的节点划出了屏幕及父组件预加载的范围。当UI主线程空闲时,会去检测template type等于a的缓存池是否还有空间,此时缓存池中有四个节点,超过了额定的3个,Repeat会释放掉最后一个节点。
166
167![Repeat-Slide-Done](./figures/Repeat-Slide-Done.PNG)
168
169#### 数据更新场景
170
171![Repeat-Start](./figures/Repeat-Start.PNG)
172
173此时我们做如下更新操作,删除index=12节点,更新index=13节点的数据,更新index=14节点的template type为a,更新index=15节点的key。
174
175![Repeat-Update1](./figures/Repeat-Update1.PNG)
176
177此时Repeat会通知父组件重新布局,逐一对比template type值,若和原节点template type值相同,则复用该节点,更新key、index和data,若template type值发生变化,则复用相应template type的缓存池中的节点,并更新key、index和data。
178
179![Repeat-Update2](./figures/Repeat-Update2.PNG)
180
181上图显示node13节点更新了数据data和index;node14更新了template type和index,于是从缓存池中取走一个复用;node15由于key值发生变化并且template type不变,复用自身节点并同步更新key、index、data;node16和node17均只更新index。index=17的节点是新的,从缓存池中复用。
182
183![Repeat-Update-Done](./figures/Repeat-Update-Done.PNG)
184
185### template:子组件渲染模板
186
187template模板目前只支持在virtualScroll模式下使用。
188
189- 每个节点会根据`.templateId()`得到template type,从而渲染对应的`.template()`中的子组件。
190- 当多个template type相同时,Repeat会覆盖先定义的`.template()`函数,仅生效最后定义的`.template()`。
191- 如果找不到对应的template type,Repeat会优先渲染type为空的`.template()`中的子组件,如果没有,则渲染`.each()`中的子组件。
192
193### totalCount:期望加载的数据长度
194
195totalCount表示期望加载的数据长度,默认为原数组长度,可以大于已加载数据项的数量。令arr.length表示数据源长度,以下为totalCount的处理规则:
196
197- totalCount缺省/非自然数时,totalCount默认为arr.length,列表正常滚动。
198- 0 <= totalCount < arr.length时,界面中只渲染“totalCount”个数据。
199- totalCount > arr.length时,代表Repeat将渲染totalCount个数据,滚动条样式根据totalCount值变化。
200
201> **注意:**
202>
203> 当totalCount > arr.length时,在父组件容器滚动过程中,应用需要保证列表即将滑动到数据源末尾时请求后续数据,开发者需要对数据请求的错误场景(如网络延迟)进行保护操作,直到数据源全部加载完成,否则列表滑动的过程中会出现滚动效果异常。
204
205### cachedCount:空闲节点缓存列表大小
206
207cachedCount是相应的template type的缓存池中可缓存子组件节点的最大数量,仅在virtualScroll模式下生效。
208
209> **说明:**
210>
211> 滚动容器组件属性`.cachedCount()`和`.template()`的参数`cachedCount`都是为了平衡性能和内存,但是含义是不同的。
212> - 滚动类容器组件`.cachedCount()`:是指在可见范围外预加载的节点,这些节点会位于组件树上,但不是可见范围内,List/Grid等容器组件会额外渲染这些可见范围外的节点,从而达到其性能收益。Repeat会将这些节点视为“可见”的。
213> - `.template()`中的`cachedCount`: 是指Repeat视为“不可见”的节点,这些节点是空闲的,框架会暂时保存,在需要使用的时候更新这些节点,从而实现复用。
214
215将cachedCount设置为当前模板的节点在屏上可能出现的最大数量时,Repeat可以做到尽可能多的复用。但后果是当屏上没有当前模板的节点时,缓存池也不会释放,应用内存会增大。需要开发者根据具体情况自行把控。
216
217- cachedCount缺省时,框架会分别对不同template,根据屏上节点+预加载的节点个数来计算cachedCount。当屏上节点+预加载的节点个数变多时,cachedCount也会对应增长。需要注意cachedCount数量不会减少。
218- 显式指定cachedCount,推荐设置成和屏幕上节点个数一致。需要注意,不推荐设置cachedCount小于2,因为这会导致在快速滑动场景下创建新的节点,从而导致性能劣化。
219
220## 使用场景
221
222### non-virtualScroll数据展示&操作
223
224#### 数据源变化
225
226```ts
227@Entry
228@ComponentV2
229struct Parent {
230  @Local simpleList: Array<string> = ['one', 'two', 'three'];
231
232  build() {
233    Row() {
234      Column() {
235        Text('点击修改第3个数组项的值')
236          .fontSize(24)
237          .fontColor(Color.Red)
238          .onClick(() => {
239            this.simpleList[2] = 'new three';
240          })
241
242        Repeat<string>(this.simpleList)
243            .each((obj: RepeatItem<string>)=>{
244              ChildItem({ item: obj.item })
245                .margin({top: 20})
246            })
247            .key((item: string) => item)
248      }
249      .justifyContent(FlexAlign.Center)
250      .width('100%')
251      .height('100%')
252    }
253    .height('100%')
254    .backgroundColor(0xF1F3F5)
255  }
256}
257
258@ComponentV2
259struct ChildItem {
260  @Param @Require item: string;
261
262  build() {
263    Text(this.item)
264      .fontSize(30)
265  }
266}
267```
268
269![ForEach-Non-Initial-Render-Case-Effect](./figures/ForEach-Non-Initial-Render-Case-Effect.gif)
270
271第三个数组项重新渲染时会复用之前的第三项的组件,仅对数据做了刷新。
272
273#### 索引值变化
274
275下方例子当交换数组项1和2时,若键值和上次保持一致,Repeat会复用之前的组件,仅对使用了index索引值的组件做数据刷新。
276
277```ts
278@Entry
279@ComponentV2
280struct Parent {
281  @Local simpleList: Array<string> = ['one', 'two', 'three'];
282
283  build() {
284    Row() {
285      Column() {
286        Text('交换数组项1,2')
287          .fontSize(24)
288          .fontColor(Color.Red)
289          .onClick(() => {
290            let temp: string = this.simpleList[2]
291            this.simpleList[2] = this.simpleList[1]
292            this.simpleList[1] = temp
293          })
294          .margin({bottom: 20})
295
296        Repeat<string>(this.simpleList)
297          .each((obj: RepeatItem<string>)=>{
298            Text("index: " + obj.index)
299              .fontSize(30)
300            ChildItem({ item: obj.item })
301              .margin({bottom: 20})
302          })
303          .key((item: string) => item)
304      }
305      .justifyContent(FlexAlign.Center)
306      .width('100%')
307      .height('100%')
308    }
309    .height('100%')
310    .backgroundColor(0xF1F3F5)
311  }
312}
313
314@ComponentV2
315struct ChildItem {
316  @Param @Require item: string;
317
318  build() {
319    Text(this.item)
320      .fontSize(30)
321  }
322}
323```
324
325![Repeat-Non-Initial-Render-Case-Exchange-Effect](./figures/Repeat-Non-Initial-Render-Case-Exchange-Effect.gif)
326
327### virtualScroll数据展示&操作
328
329本小节将展示virtualScroll模式下,Repeat的实际使用场景和组件节点的复用情况。根据复用规则可以衍生出大量的测试场景,篇幅原因,只对典型的数据变化进行解释。
330
331#### 一个template
332
333下面的代码示例展示了Repeat virtualScroll模式下修改数组的常见操作,包括**插入数据、修改数据、删除数据、交换数据**。点击下拉框选择索引index值,点击相应的按钮即可进行数据修改操作。依次点击数据项可以交换被点击的两个数据项。
334
335```ts
336@ObservedV2
337class Repeat005Clazz {
338  @Trace message: string = '';
339
340  constructor(message: string) {
341    this.message = message;
342  }
343}
344
345@Entry
346@ComponentV2
347struct RepeatVirtualScroll {
348  @Local simpleList: Array<Repeat005Clazz> = [];
349  private exchange: number[] = [];
350  private counter: number = 0;
351  @Local selectOptions: SelectOption[] = [];
352  @Local selectIdx: number = 0;
353
354  @Monitor("simpleList")
355  reloadSelectOptions(): void {
356    this.selectOptions = [];
357    for (let i = 0; i < this.simpleList.length; ++i) {
358      this.selectOptions.push({ value: i.toString() });
359    }
360    if (this.selectIdx >= this.simpleList.length) {
361      this.selectIdx = this.simpleList.length - 1;
362    }
363  }
364
365  aboutToAppear(): void {
366    for (let i = 0; i < 100; i++) {
367      this.simpleList.push(new Repeat005Clazz(`item_${i}`));
368    }
369    this.reloadSelectOptions();
370  }
371
372  handleExchange(idx: number): void { // 点击交换子组件
373    this.exchange.push(idx);
374    if (this.exchange.length === 2) {
375      let _a = this.exchange[0];
376      let _b = this.exchange[1];
377      let temp: Repeat005Clazz = this.simpleList[_a];
378      this.simpleList[_a] = this.simpleList[_b];
379      this.simpleList[_b] = temp;
380      this.exchange = [];
381    }
382  }
383
384  build() {
385    Column({ space: 10 }) {
386      Text('virtualScroll each()&template() 1t')
387        .fontSize(15)
388        .fontColor(Color.Gray)
389      Text('Select an index and press the button to update data.')
390        .fontSize(15)
391        .fontColor(Color.Gray)
392
393      Select(this.selectOptions)
394        .selected(this.selectIdx)
395        .value(this.selectIdx.toString())
396        .key('selectIdx')
397        .onSelect((index: number) => {
398          this.selectIdx = index;
399        })
400      Row({ space: 5 }) {
401        Button('Add No.' + this.selectIdx)
402          .onClick(() => {
403            this.simpleList.splice(this.selectIdx, 0, new Repeat005Clazz(`${this.counter++}_add_item`));
404            this.reloadSelectOptions();
405          })
406        Button('Modify No.' + this.selectIdx)
407          .onClick(() => {
408            this.simpleList.splice(this.selectIdx, 1, new Repeat005Clazz(`${this.counter++}_modify_item`));
409          })
410        Button('Del No.' + this.selectIdx)
411          .onClick(() => {
412            this.simpleList.splice(this.selectIdx, 1);
413            this.reloadSelectOptions();
414          })
415      }
416      Button('Update array length to 5.')
417        .onClick(() => {
418          this.simpleList = this.simpleList.slice(0, 5);
419          this.reloadSelectOptions();
420        })
421
422      Text('Click on two items to exchange.')
423        .fontSize(15)
424        .fontColor(Color.Gray)
425
426      List({ space: 10 }) {
427        Repeat<Repeat005Clazz>(this.simpleList)
428          .each((obj: RepeatItem<Repeat005Clazz>) => {
429            ListItem() {
430              Text(`[each] index${obj.index}: ${obj.item.message}`)
431                .fontSize(25)
432                .onClick(() => {
433                  this.handleExchange(obj.index);
434                })
435            }
436          })
437          .key((item: Repeat005Clazz, index: number) => {
438            return item.message;
439          })
440          .virtualScroll({ totalCount: this.simpleList.length })
441          .templateId(() => "a")
442          .template('a', (ri) => {
443            Text(`[a] index${ri.index}: ${ri.item.message}`)
444              .fontSize(25)
445              .onClick(() => {
446                this.handleExchange(ri.index);
447              })
448          }, { cachedCount: 3 })
449      }
450      .cachedCount(2)
451      .border({ width: 1 })
452      .width('95%')
453      .height('40%')
454    }
455    .justifyContent(FlexAlign.Center)
456    .width('100%')
457    .height('100%')
458  }
459}
460```
461该应用列表内容为100项自定义类`RepeatClazz`的`message`字符串属性,List组件的cachedCount设为2,模板'a'的缓存池大小设为3。应用界面如下图所示:
462
463![Repeat-VirtualScroll-Demo](./figures/Repeat-VirtualScroll-Demo.gif)
464
465#### 多个template
466
467```ts
468@ObservedV2
469class Repeat006Clazz {
470  @Trace message: string = '';
471
472  constructor(message: string) {
473    this.message = message;
474  }
475}
476
477@Entry
478@ComponentV2
479struct RepeatVirtualScroll2T {
480  @Local simpleList: Array<Repeat006Clazz> = [];
481  private exchange: number[] = [];
482  private counter: number = 0;
483  @Local selectOptions: SelectOption[] = [];
484  @Local selectIdx: number = 0;
485
486  @Monitor("simpleList")
487  reloadSelectOptions(): void {
488    this.selectOptions = [];
489    for (let i = 0; i < this.simpleList.length; ++i) {
490      this.selectOptions.push({ value: i.toString() });
491    }
492    if (this.selectIdx >= this.simpleList.length) {
493      this.selectIdx = this.simpleList.length - 1;
494    }
495  }
496
497  aboutToAppear(): void {
498    for (let i = 0; i < 100; i++) {
499      this.simpleList.push(new Repeat006Clazz(`item_${i}`));
500    }
501    this.reloadSelectOptions();
502  }
503
504  handleExchange(idx: number): void { // 点击交换子组件
505    this.exchange.push(idx);
506    if (this.exchange.length === 2) {
507      let _a = this.exchange[0];
508      let _b = this.exchange[1];
509      let temp: Repeat006Clazz = this.simpleList[_a];
510      this.simpleList[_a] = this.simpleList[_b];
511      this.simpleList[_b] = temp;
512      this.exchange = [];
513    }
514  }
515
516  build() {
517    Column({ space: 10 }) {
518      Text('virtualScroll each()&template() 2t')
519        .fontSize(15)
520        .fontColor(Color.Gray)
521      Text('Select an index and press the button to update data.')
522        .fontSize(15)
523        .fontColor(Color.Gray)
524
525      Select(this.selectOptions)
526        .selected(this.selectIdx)
527        .value(this.selectIdx.toString())
528        .key('selectIdx')
529        .onSelect((index: number) => {
530          this.selectIdx = index;
531        })
532      Row({ space: 5 }) {
533        Button('Add No.' + this.selectIdx)
534          .onClick(() => {
535            this.simpleList.splice(this.selectIdx, 0, new Repeat006Clazz(`${this.counter++}_add_item`));
536            this.reloadSelectOptions();
537          })
538        Button('Modify No.' + this.selectIdx)
539          .onClick(() => {
540            this.simpleList.splice(this.selectIdx, 1, new Repeat006Clazz(`${this.counter++}_modify_item`));
541          })
542        Button('Del No.' + this.selectIdx)
543          .onClick(() => {
544            this.simpleList.splice(this.selectIdx, 1);
545            this.reloadSelectOptions();
546          })
547      }
548      Button('Update array length to 5.')
549        .onClick(() => {
550          this.simpleList = this.simpleList.slice(0, 5);
551          this.reloadSelectOptions();
552        })
553
554      Text('Click on two items to exchange.')
555        .fontSize(15)
556        .fontColor(Color.Gray)
557
558      List({ space: 10 }) {
559        Repeat<Repeat006Clazz>(this.simpleList)
560          .each((obj: RepeatItem<Repeat006Clazz>) => {
561            ListItem() {
562              Text(`[each] index${obj.index}: ${obj.item.message}`)
563                .fontSize(25)
564                .onClick(() => {
565                  this.handleExchange(obj.index);
566                })
567            }
568          })
569          .key((item: Repeat006Clazz, index: number) => {
570            return item.message;
571          })
572          .virtualScroll({ totalCount: this.simpleList.length })
573          .templateId((item: Repeat006Clazz, index: number) => {
574            return (index % 2 === 0) ? 'odd' : 'even';
575          })
576          .template('odd', (ri) => {
577            Text(`[odd] index${ri.index}: ${ri.item.message}`)
578              .fontSize(25)
579              .fontColor(Color.Blue)
580              .onClick(() => {
581                this.handleExchange(ri.index);
582              })
583          }, { cachedCount: 3 })
584          .template('even', (ri) => {
585            Text(`[even] index${ri.index}: ${ri.item.message}`)
586              .fontSize(25)
587              .fontColor(Color.Green)
588              .onClick(() => {
589                this.handleExchange(ri.index);
590              })
591          }, { cachedCount: 1 })
592      }
593      .cachedCount(2)
594      .border({ width: 1 })
595      .width('95%')
596      .height('40%')
597    }
598    .justifyContent(FlexAlign.Center)
599    .width('100%')
600    .height('100%')
601  }
602}
603```
604
605![Repeat-VirtualScroll-2T-Demo](./figures/Repeat-VirtualScroll-2T-Demo.gif)
606
607### Repeat嵌套
608
609Repeat支持嵌套使用。下面是使用virtualScroll模式进行嵌套的示例代码:
610
611```ts
612// Repeat嵌套
613@Entry
614@ComponentV2
615struct RepeatNest {
616  @Local outerList: string[] = [];
617  @Local innerList: number[] = [];
618
619  aboutToAppear(): void {
620    for (let i = 0; i < 20; i++) {
621      this.outerList.push(i.toString());
622      this.innerList.push(i);
623    }
624  }
625
626  build() {
627    Column({ space: 20 }) {
628      Text('Repeat virtualScroll嵌套')
629        .fontSize(15)
630        .fontColor(Color.Gray)
631      List() {
632        Repeat<string>(this.outerList)
633          .each((obj) => {
634            ListItem() {
635              Column() {
636                Text('outerList item: ' + obj.item)
637                  .fontSize(30)
638                List() {
639                  Repeat<number>(this.innerList)
640                    .each((subObj) => {
641                      ListItem() {
642                        Text('innerList item: ' + subObj.item)
643                          .fontSize(20)
644                      }
645                    })
646                    .key((item) => "innerList_" + item)
647                    .virtualScroll()
648                }
649                .width('80%')
650                .border({ width: 1 })
651                .backgroundColor(Color.Orange)
652              }
653              .height('30%')
654              .backgroundColor(Color.Pink)
655            }
656            .border({ width: 1 })
657          })
658          .key((item) => "outerList_" + item)
659          .virtualScroll()
660      }
661      .width('80%')
662      .border({ width: 1 })
663    }
664    .justifyContent(FlexAlign.Center)
665    .width('90%')
666    .height('80%')
667  }
668}
669```
670
671运行效果:
672
673![Repeat-Nest](./figures/Repeat-Nest.png)
674
675### 父容器组件应用场景
676
677本节展示Repeat virtualScroll模式与容器组件的常见应用场景。
678
679#### 与List组合使用
680
681在List容器组件中使用Repeat的virtualScroll模式,示例如下:
682
683```ts
684class DemoListItemInfo {
685  name: string;
686  icon: Resource;
687
688  constructor(name: string, icon: Resource) {
689    this.name = name;
690    this.icon = icon;
691  }
692}
693
694@Entry
695@ComponentV2
696struct DemoList {
697  @Local videoList: Array<DemoListItemInfo> = [];
698
699  aboutToAppear(): void {
700    for (let i = 0; i < 10; i++) {
701      // 此处app.media.listItem0app.media.listItem1app.media.listItem2仅作示例,请开发者自行替换
702      this.videoList.push(new DemoListItemInfo('视频' + i,
703        i % 3 == 0 ? $r("app.media.listItem0") :
704        i % 3 == 1 ? $r("app.media.listItem1") : $r("app.media.listItem2")));
705    }
706  }
707
708  @Builder
709  itemEnd(index: number) {
710    Button('删除')
711      .backgroundColor(Color.Red)
712      .onClick(() => {
713        this.videoList.splice(index, 1);
714      })
715  }
716
717  build() {
718    Column({ space: 10 }) {
719      Text('List容器组件中包含Repeat组件')
720        .fontSize(15)
721        .fontColor(Color.Gray)
722
723      List({ space: 5 }) {
724        Repeat<DemoListItemInfo>(this.videoList)
725          .each((obj: RepeatItem<DemoListItemInfo>) => {
726            ListItem() {
727              Column() {
728                Image(obj.item.icon)
729                  .width('80%')
730                  .margin(10)
731                Text(obj.item.name)
732                  .fontSize(20)
733              }
734            }
735            .swipeAction({
736              end: {
737                builder: () => {
738                  this.itemEnd(obj.index);
739                }
740              }
741            })
742            .onAppear(() => {
743              console.info('AceTag', obj.item.name);
744            })
745          })
746          .key((item: DemoListItemInfo) => item.name)
747          .virtualScroll()
748      }
749      .cachedCount(2)
750      .height('90%')
751      .border({ width: 1 })
752      .listDirection(Axis.Vertical)
753      .alignListItem(ListItemAlign.Center)
754      .divider({
755        strokeWidth: 1,
756        startMargin: 60,
757        endMargin: 60,
758        color: '#ffe9f0f0'
759      })
760
761      Row({ space: 10 }) {
762        Button('删除第1项')
763          .onClick(() => {
764            this.videoList.splice(0, 1);
765          })
766        Button('删除第5项')
767          .onClick(() => {
768            this.videoList.splice(4, 1);
769          })
770      }
771    }
772    .width('100%')
773    .height('100%')
774    .justifyContent(FlexAlign.Center)
775  }
776}
777```
778
779右滑并点击按钮,或点击底部按钮,可删除视频卡片:
780
781![Repeat-Demo-List](./figures/Repeat-Demo-List.gif)
782
783#### 与Grid组合使用
784
785在Grid容器组件中使用Repeat的virtualScroll模式,示例如下:
786
787```ts
788class DemoGridItemInfo {
789  name: string;
790  icon: Resource;
791
792  constructor(name: string, icon: Resource) {
793    this.name = name;
794    this.icon = icon;
795  }
796}
797
798@Entry
799@ComponentV2
800struct DemoGrid {
801  @Local itemList: Array<DemoGridItemInfo> = [];
802  @Local isRefreshing: boolean = false;
803  private layoutOptions: GridLayoutOptions = {
804    regularSize: [1, 1],
805    irregularIndexes: [10]
806  }
807  private GridScroller: Scroller = new Scroller();
808  private num: number = 0;
809
810  aboutToAppear(): void {
811    for (let i = 0; i < 10; i++) {
812      // 此处app.media.gridItem0app.media.gridItem1app.media.gridItem2仅作示例,请开发者自行替换
813      this.itemList.push(new DemoGridItemInfo('视频' + i,
814        i % 3 == 0 ? $r("app.media.gridItem0") :
815        i % 3 == 1 ? $r("app.media.gridItem1") : $r("app.media.gridItem2")));
816    }
817  }
818
819  build() {
820    Column({ space: 10 }) {
821      Text('Grid容器组件中包含Repeat组件')
822        .fontSize(15)
823        .fontColor(Color.Gray)
824
825      Refresh({ refreshing: $$this.isRefreshing }) {
826        Grid(this.GridScroller, this.layoutOptions) {
827          Repeat<DemoGridItemInfo>(this.itemList)
828            .each((obj: RepeatItem<DemoGridItemInfo>) => {
829              if (obj.index === 10 ) {
830                GridItem() {
831                  Text('先前浏览至此,点击刷新')
832                    .fontSize(20)
833                }
834                .height(30)
835                .border({ width: 1 })
836                .onClick(() => {
837                  this.GridScroller.scrollToIndex(0);
838                  this.isRefreshing = true;
839                })
840                .onAppear(() => {
841                  console.info('AceTag', obj.item.name);
842                })
843              } else {
844                GridItem() {
845                  Column() {
846                    Image(obj.item.icon)
847                      .width('100%')
848                      .height(80)
849                      .objectFit(ImageFit.Cover)
850                      .borderRadius({ topLeft: 16, topRight: 16 })
851                    Text(obj.item.name)
852                      .fontSize(15)
853                      .height(20)
854                  }
855                }
856                .height(100)
857                .borderRadius(16)
858                .backgroundColor(Color.White)
859                .onAppear(() => {
860                  console.info('AceTag', obj.item.name);
861                })
862              }
863            })
864            .key((item: DemoGridItemInfo) => item.name)
865            .virtualScroll()
866        }
867        .columnsTemplate('repeat(auto-fit, 150)')
868        .cachedCount(4)
869        .rowsGap(15)
870        .columnsGap(10)
871        .height('100%')
872        .padding(10)
873        .backgroundColor('#F1F3F5')
874      }
875      .onRefreshing(() => {
876        setTimeout(() => {
877          this.itemList.splice(10, 1);
878          this.itemList.unshift(new DemoGridItemInfo('refresh', $r('app.media.gridItem0'))); // 此处app.media.gridItem0仅作示例,请开发者自行替换
879          for (let i = 0; i < 10; i++) {
880            // 此处aapp.media.gridItem0app.media.gridItem1app.media.gridItem2仅作示例,请开发者自行替换
881            this.itemList.unshift(new DemoGridItemInfo('新视频' + this.num,
882              i % 3 == 0 ? $r("app.media.gridItem0") :
883              i % 3 == 1 ? $r("app.media.gridItem1") : $r("app.media.gridItem2")));
884            this.num++;
885          }
886          this.isRefreshing = false;
887        }, 1000);
888        console.info('AceTag', 'onRefreshing');
889      })
890      .refreshOffset(64)
891      .pullToRefresh(true)
892      .width('100%')
893      .height('85%')
894
895      Button('刷新')
896        .onClick(() => {
897          this.GridScroller.scrollToIndex(0);
898          this.isRefreshing = true;
899        })
900    }
901    .width('100%')
902    .height('100%')
903    .justifyContent(FlexAlign.Center)
904  }
905}
906```
907
908下拉屏幕,或点击刷新按钮,或点击“先前浏览至此,点击刷新”,可加载新的视频内容:
909
910![Repeat-Demo-Grid](./figures/Repeat-Demo-Grid.gif)
911
912#### 与Swiper组合使用
913
914在Swiper容器组件中使用Repeat的virtualScroll模式,示例如下:
915
916```ts
917const remotePictures: Array<string> = [
918  'https://www.example.com/xxx/0001.jpg', // 请填写具体的网络图片地址
919  'https://www.example.com/xxx/0002.jpg',
920  'https://www.example.com/xxx/0003.jpg',
921  'https://www.example.com/xxx/0004.jpg',
922  'https://www.example.com/xxx/0005.jpg',
923  'https://www.example.com/xxx/0006.jpg',
924  'https://www.example.com/xxx/0007.jpg',
925  'https://www.example.com/xxx/0008.jpg',
926  'https://www.example.com/xxx/0009.jpg'
927];
928
929@ObservedV2
930class DemoSwiperItemInfo {
931  id: string;
932  @Trace url: string = 'default';
933
934  constructor(id: string) {
935    this.id = id;
936  }
937}
938
939@Entry
940@ComponentV2
941struct DemoSwiper {
942  @Local pics: Array<DemoSwiperItemInfo> = [];
943
944  aboutToAppear(): void {
945    for (let i = 0; i < 9; i++) {
946      this.pics.push(new DemoSwiperItemInfo('pic' + i));
947    }
948    setTimeout(() => {
949      this.pics[0].url = remotePictures[0];
950    }, 1000);
951  }
952
953  build() {
954    Column() {
955      Text('Swiper容器组件中包含Repeat组件')
956        .fontSize(15)
957        .fontColor(Color.Gray)
958
959      Stack() {
960        Text('图片加载中')
961          .fontSize(15)
962          .fontColor(Color.Gray)
963        Swiper() {
964          Repeat(this.pics)
965            .each((obj: RepeatItem<DemoSwiperItemInfo>) => {
966              Image(obj.item.url)
967                .onAppear(() => {
968                  console.info('AceTag', obj.item.id);
969                })
970            })
971            .key((item: DemoSwiperItemInfo) => item.id)
972            .virtualScroll()
973        }
974        .cachedCount(9)
975        .height('50%')
976        .loop(false)
977        .indicator(true)
978        .onChange((index) => {
979          setTimeout(() => {
980            this.pics[index].url = remotePictures[index];
981          }, 1000);
982        })
983      }
984      .width('100%')
985      .height('100%')
986      .backgroundColor(Color.Black)
987    }
988  }
989}
990```
991
992定时1秒后加载图片,模拟网络延迟:
993
994![Repeat-Demo-Swiper](./figures/Repeat-Demo-Swiper.gif)
995
996## 常见问题
997
998### 屏幕外的列表数据发生变化时,保证滚动条位置不变
999
1000在List组件中声明Repeat组件,实现key值生成逻辑和each逻辑(如下示例代码),点击按钮“insert”,在屏幕显示的第一个元素前面插入一个元素,屏幕出现向下滚动。
1001
1002```ts
1003// 定义一个类,标记为可观察的
1004// 类中自定义一个数组,标记为可追踪的
1005@ObservedV2
1006class ArrayHolder {
1007  @Trace arr: Array<number> = [];
1008
1009  // constructor,用于初始化数组个数
1010  constructor(count: number) {
1011    for (let i = 0; i < count; i++) {
1012      this.arr.push(i);
1013    }
1014  }
1015}
1016
1017@Entry
1018@ComponentV2
1019struct RepeatTemplateSingle {
1020  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
1021  @Local totalCount: number = this.arrayHolder.arr.length;
1022  scroller: Scroller = new Scroller();
1023
1024  build() {
1025    Column({ space: 5 }) {
1026      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
1027        Repeat(this.arrayHolder.arr)
1028          .virtualScroll({ totalCount: this.totalCount })
1029          .templateId((item, index) => {
1030            return 'number';
1031          })
1032          .template('number', (r) => {
1033            ListItem() {
1034              Text(r.index! + ":" + r.item + "Reuse");
1035            }
1036          })
1037          .each((r) => {
1038            ListItem() {
1039              Text(r.index! + ":" + r.item + "eachMessage");
1040            }
1041          })
1042      }
1043      .height('30%')
1044
1045      Button(`insert totalCount ${this.totalCount}`)
1046        .height(60)
1047        .onClick(() => {
1048          // 插入元素,元素位置为屏幕显示的前一个元素
1049          this.arrayHolder.arr.splice(18, 0, this.totalCount);
1050          this.totalCount = this.arrayHolder.arr.length;
1051        })
1052    }
1053    .width('100%')
1054    .margin({ top: 5 })
1055  }
1056}
1057```
1058
1059运行效果:
1060
1061![Repeat-case1-Error](./figures/Repeat-Case1-Error.gif)
1062
1063在一些场景中,我们不希望屏幕外的数据源变化影响屏幕中List列表Scroller停留的位置,可以通过List组件的[onScrollIndex](../ui/arkts-layout-development-create-list.md#响应滚动位置)事件对列表滚动动作进行监听,当列表发生滚动时,获取列表滚动位置。使用Scroller组件的[scrollToIndex](../reference/apis-arkui/arkui-ts/ts-container-scroll.md#scrolltoindex)特性,滑动到指定index位置,实现屏幕外的数据源增加/删除数据时,Scroller停留的位置不变的效果。
1064
1065示例代码仅对增加数据的情况进行展示。
1066
1067```ts
1068// ...ArrayHolder的定义和上述demo代码一致
1069
1070@Entry
1071@ComponentV2
1072struct RepeatTemplateSingle {
1073  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
1074  @Local totalCount: number = this.arrayHolder.arr.length;
1075  scroller: Scroller = new Scroller();
1076
1077  private start: number = 1;
1078  private end: number = 1;
1079
1080  build() {
1081    Column({ space: 5 }) {
1082      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
1083        Repeat(this.arrayHolder.arr)
1084          .virtualScroll({ totalCount: this.totalCount })
1085          .templateId((item, index) => {
1086            return 'number';
1087          })
1088          .template('number', (r) => {
1089            ListItem() {
1090              Text(r.index! + ":" + r.item + "Reuse");
1091            }
1092          })
1093          .each((r) => {
1094            ListItem() {
1095              Text(r.index! + ":" + r.item + "eachMessage");
1096            }
1097          })
1098      }
1099      .onScrollIndex((start, end) => {
1100        this.start = start;
1101        this.end = end;
1102      })
1103      .height('30%')
1104
1105      Button(`insert totalCount ${this.totalCount}`)
1106        .height(60)
1107        .onClick(() => {
1108          // 插入元素,元素位置为屏幕显示的前一个元素
1109          this.arrayHolder.arr.splice(18, 0, this.totalCount);
1110          let rect = this.scroller.getItemRect(this.start); // 获取子组件的大小位置
1111          this.scroller.scrollToIndex(this.start + 1); // 滑动到指定index
1112          this.scroller.scrollBy(0, -rect.y); // 滑动指定距离
1113          this.totalCount = this.arrayHolder.arr.length;
1114        })
1115    }
1116    .width('100%')
1117    .margin({ top: 5 })
1118  }
1119}
1120```
1121
1122运行效果:
1123
1124![Repeat-case1-Succ](./figures/Repeat-Case1-Succ.gif)
1125
1126### totalCount值大于数据源长度
1127
1128当数据源总长度很大时,会使用懒加载的方式先加载一部分数据,为了使Repeat显示正确的滚动条样式,需要将数据总长度赋值给totalCount,即数据源全部加载完成前,totalCount大于array.length1129
1130totalCount > array.length时,在父组件容器滚动过程中,应用需要保证列表即将滑动到数据源末尾时请求后续数据,开发者需要对数据请求的错误场景(如网络延迟)进行保护操作,直到数据源全部加载完成,否则列表滑动的过程中会出现滚动效果异常。
1131
1132上述规范可以通过实现父组件List/Grid的[onScrollIndex](../ui/arkts-layout-development-create-list.md#响应滚动位置)属性的回调函数完成。示例代码如下:
1133
1134```ts
1135@ObservedV2
1136class VehicleData {
1137  @Trace name: string;
1138  @Trace price: number;
1139
1140  constructor(name: string, price: number) {
1141    this.name = name;
1142    this.price = price;
1143  }
1144}
1145
1146@ObservedV2
1147class VehicleDB {
1148  public vehicleItems: VehicleData[] = [];
1149
1150  constructor() {
1151    // 数组初始化大小 20
1152    for (let i = 1; i <= 20; i++) {
1153      this.vehicleItems.push(new VehicleData(`Vehicle${i}`, i));
1154    }
1155  }
1156}
1157
1158@Entry
1159@ComponentV2
1160struct entryCompSucc {
1161  @Local vehicleItems: VehicleData[] = new VehicleDB().vehicleItems;
1162  @Local listChildrenSize: ChildrenMainSize = new ChildrenMainSize(60);
1163  @Local totalCount: number = this.vehicleItems.length;
1164  scroller: Scroller = new Scroller();
1165
1166  build() {
1167    Column({ space: 3 }) {
1168      List({ scroller: this.scroller }) {
1169        Repeat(this.vehicleItems)
1170          .virtualScroll({ totalCount: 50 }) // 数组预期长度 50
1171          .templateId(() => 'default')
1172          .template('default', (ri) => {
1173            ListItem() {
1174              Column() {
1175                Text(`${ri.item.name} + ${ri.index}`)
1176                  .width('90%')
1177                  .height(this.listChildrenSize.childDefaultSize)
1178                  .backgroundColor(0xFFA07A)
1179                  .textAlign(TextAlign.Center)
1180                  .fontSize(20)
1181                  .fontWeight(FontWeight.Bold)
1182              }
1183            }.border({ width: 1 })
1184          }, { cachedCount: 5 })
1185          .each((ri) => {
1186            ListItem() {
1187              Text("Wrong: " + `${ri.item.name} + ${ri.index}`)
1188                .width('90%')
1189                .height(this.listChildrenSize.childDefaultSize)
1190                .backgroundColor(0xFFA07A)
1191                .textAlign(TextAlign.Center)
1192                .fontSize(20)
1193                .fontWeight(FontWeight.Bold)
1194            }.border({ width: 1 })
1195          })
1196          .key((item, index) => `${index}:${item}`)
1197      }
1198      .height('50%')
1199      .margin({ top: 20 })
1200      .childrenMainSize(this.listChildrenSize)
1201      .alignListItem(ListItemAlign.Center)
1202      .onScrollIndex((start, end) => {
1203        console.log('onScrollIndex', start, end);
1204        // 数据懒加载
1205        if (this.vehicleItems.length < 50) {
1206          for (let i = 0; i < 10; i++) {
1207            if (this.vehicleItems.length < 50) {
1208              this.vehicleItems.push(new VehicleData("Vehicle_loaded", i));
1209            }
1210          }
1211        }
1212      })
1213    }
1214  }
1215}
1216```
1217
1218示例代码运行效果:
1219
1220![Repeat-Case2-Succ](./figures/Repeat-Case2-Succ.gif)
1221
1222### Repeat与@Builder混用的限制
1223
1224当Repeat与@Builder混用时,必须将RepeatItem类型整体进行传参,组件才能监听到数据变化,如果只传递`RepeatItem.item`或`RepeatItem.index`,将会出现UI渲染异常。
1225
1226示例代码如下:
1227
1228```ts
1229@Entry
1230@ComponentV2
1231struct RepeatBuilderPage {
1232  @Local simpleList1: Array<number> = [];
1233  @Local simpleList2: Array<number> = [];
1234
1235  aboutToAppear(): void {
1236    for (let i = 0; i < 100; i++) {
1237      this.simpleList1.push(i)
1238      this.simpleList2.push(i)
1239    }
1240  }
1241
1242  build() {
1243    Column({ space: 20 }) {
1244      Text('Repeat与@Builder混用,左边是异常场景,右边是正常场景,向下滑动一段距离可以看出差别')
1245        .fontSize(15)
1246        .fontColor(Color.Gray)
1247
1248      Row({ space: 20 }) {
1249        List({ initialIndex: 5, space: 20 }) {
1250          Repeat<number>(this.simpleList1)
1251            .each((ri) => {})
1252            .virtualScroll({ totalCount: this.simpleList1.length })
1253            .templateId((item: number, index: number) => "default")
1254            .template('default', (ri) => {
1255              ListItem() {
1256                Column() {
1257                  Text('Text id = ' + ri.item)
1258                    .fontSize(20)
1259                  this.buildItem1(ri.item) // 修改为:this.buildItem1(ri)
1260                }
1261              }
1262              .border({ width: 1 })
1263            }, { cachedCount: 3 })
1264        }
1265        .cachedCount(1)
1266        .border({ width: 1 })
1267        .width('45%')
1268        .height('60%')
1269
1270        List({ initialIndex: 5, space: 20 }) {
1271          Repeat<number>(this.simpleList2)
1272            .each((ri) => {})
1273            .virtualScroll({ totalCount: this.simpleList2.length })
1274            .templateId((item: number, index: number) => "default")
1275            .template('default', (ri) => {
1276              ListItem() {
1277                Column() {
1278                  Text('Text id = ' + ri.item)
1279                    .fontSize(20)
1280                  this.buildItem2(ri)
1281                }
1282              }
1283              .border({ width: 1 })
1284            }, { cachedCount: 3 })
1285        }
1286        .cachedCount(1)
1287        .border({ width: 1 })
1288        .width('45%')
1289        .height('60%')
1290      }
1291    }
1292    .height('100%')
1293    .justifyContent(FlexAlign.Center)
1294  }
1295
1296  @Builder
1297  // @Builder参数必须传RepeatItem类型才能正常渲染
1298  buildItem1(item: number) {
1299    Text('Builder1 id = ' + item)
1300      .fontSize(20)
1301      .fontColor(Color.Red)
1302      .margin({ top: 2 })
1303  }
1304
1305  @Builder
1306  buildItem2(ri: RepeatItem<number>) {
1307    Text('Builder2 id = ' + ri.item)
1308      .fontSize(20)
1309      .fontColor(Color.Red)
1310      .margin({ top: 2 })
1311  }
1312}
1313```
1314
1315界面展示如下图,进入页面后向下滑动一段距离可以看出差别,左边是错误用法,右边是正确用法(Text组件为黑色,Builder组件为红色)。上述代码展示了开发过程中易出错的场景,即在@Builder构造函数中传参方式为值传递。
1316
1317![Repeat-Builder](./figures/Repeat-Builder.png)