1# Repeat: Reusing Child Components
2
3>**NOTE**
4>
5> Repeat is supported since API version 12.
6
7For details about API parameters, see [Repeat APIs](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md).
8
9In the non-virtualScroll scenario (that is, **virtualScroll** is disabled), **Repeat**, used together with container components, renders repeated components based on the data source. In addition, the component returned by the API must be a child component that can be contained in the **Repeat** parent container component. Compared with ForEach, **Repeat** optimizes the rendering performance in some update scenarios and generates function with the index maintained by the framework.
10
11When virtualScroll is enabled, **Repeat** iterates data from the provided data source as required and creates the corresponding component during each iteration. In this way, **Repeat** must be used together with the scrolling container component. When **Repeat** is used in the scrolling container component, the framework creates components as required based on the visible area of the scrolling container. When a component slides out of the visible area, the framework caches the component and uses it in the next iteration.
12
13## Constraints
14
15- Repeat uses key value as identifiers. Therefore, **key()** must generate a unique value for each data.
16- **Repeat virtualScroll** must be used in the scrolling container component. Only the [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), and [WaterFlow](../reference/apis-arkui/arkui-ts/ts-container-waterflow.md) components support this scenario. In this case, **cachedCount** takes effect. Other container components apply only to the non-virtualScroll scenario.
17- After **virtualScroll** is enabled for **Repeat**, only one child component can be created in each iteration. Otherwise, there is no constraint. The generated child components must be allowed in the parent container component of **Repeat**.
18- When **Repeat** and custom component (or the @Builder function) are used together, the **RepeatItem** type must be passed as a whole so that the component can listen for data changes. If only **RepeatItem.item** or **RepeatItem.index** is passed, an exception occurs during the UI rendering.
19- Currently, the template applies to scenarios where **virtualScroll** is enabled. If multiple template types are the same, **Repeat** overrides old **template()** and only the latest **template()** takes effect.
20- If the value of **totalCount** is greater than that of **array.length**, when the parent component container is scrolling, the application should ensure that subsequent data is requested when the list is about to slide to the end of the data source until all data sources are loaded. Otherwise, the scrolling effect is abnormal. For details about the solution, see [The totalCount Value Is Greater Than the Length of Data Source](#the-totalcount-value-is-greater-than-the-length-of-data-source).
21- When **Repeat** is used in a container component, only one **Repeat** can be contained. Take **List** as an example. Containing **ListItem**, **ForEach**, and **LazyForEach** together in this component, or containing multiple **Repeat** components at the same time is not recommended.
22- When **virtualScroll** is enabled, the decorators of V1 are not supported in **Repeat**. Using them together may throw an exception during rendering.
23
24## Key Generation Rules
25
26The purpose of **key()** is to allow **Repeat** to identify the details of array changes, including addition, deletion, and location (index) of data.
27
28Suggestions:
29
30- Even if there are duplicate data items (or the data source changes), you must ensure that the key is unique .
31- Each time **key()** is executed, the same data item is used as the input, and the output must be consistent.
32- Using index in **key()** is allowed, but not recommended. The reason is that the index changes when the data item is moved, that is, the key changes. Therefore, Repeat considers that the data item changes and triggers UI re-rendering, which deteriorates the performance.
33- You are advised to convert a simple array to a class object array, add a **readonly id** property, and assign a unique value to it in the constructor.
34
35### Non-virtualScroll
36
37**key()** can be left empty. **Repeat** will generate the default key.
38
39![Repeat-NonVS-KeyGen](./figures/Repeat-NonVS-KeyGen.png)
40
41### virtualScroll
42
43The rule is basically the same as that of non-virtualScroll: **key()** can be left empty.
44
45![Repeat-VS-KeyGen](./figures/Repeat-VS-KeyGen.png)
46
47## Component Generation and Reuse Rules
48
49### non-virtualScroll
50
51All child components are created when **Repeat** is rendered for the first time. The original components are reused when data is updated.
52
53When the **Repeat** component updates data, it compares all keys in the last update with those in the latest update. If the current key is the same as the last one, **Repeat** reuses the child component and updates **RepeatItem.index**.
54
55After **Repeat** compares all duplicate keys and reuses them, if the last key is unique and a new key is generated after this update, a child component needs to be created. In this case, **Repeat** will reuse redundant child components, update the **RepeatItem.item** data source and **RepeatItem.index**, and re-render the UI.
56
57If the number of remaining child components is greater than or equal to the number of newly updated components, the components are fully reused and redundant components are released. If the number of remaining child components is less than the number of newly updated components, **Repeat** will create components corresponding to the extra data items after the remaining data items are all reused.
58
59![Repeat-NonVS-FuncGen](./figures/Repeat-NonVS-FuncGen.png)
60
61### virtualScroll
62
63At the first time when **Repeat** renders child components, only the required component is generated. During sliding and data update, nodes on the lower screen are cached. When a new component needs to be generated, the cached component is reused.
64
65#### Slide shortcut
66
67The following figure describes the node state before sliding.
68
69![Repeat-Start](./figures/Repeat-Start.png)
70
71Currently, the **Repeat** component has two types of templateId. **templateId a** sets three as its maximum cache value for the corresponding cache pool. **templateId b** sets four as its maximum cache value and preloads one note for its parent components by default. Now swipe up on the screen, and **Repeat** will reuse the nodes in the cache pool.
72
73![Repeat-Slide](./figures/Repeat-Slide.png)
74
75The data of **index=18** enters the screen and the preloading range of the parent component, coming up with a result of **templateId b**. In this case, **Repeat** obtains a node from the **type=b** cache pool for reuse and updates its key, index, and data. Other grandchildren notes that use the data and index in the child node are updated based on the state management V2 rules.
76
77The **index=10** note slides out of the screen and the preloading range of the parent component. When the UI main thread is idle, it checks whether the **type=a** cache pool has sufficient space. In this case, there are four nodes in the cache pool, which exceeds the rated three, so **Repeat** will release the last node.
78
79![Repeat-Slide-Done](./figures/Repeat-Slide-Done.png)
80
81#### Data Update Scenarios
82
83![Repeat-Start](./figures/Repeat-Start.png)
84
85In this case, delete the **index=12** node, update the data of the **index=13** node, change the **templateId b** to **templateId a** of the **index=14** node, and update the key of the **index=15** node.
86
87![Repeat-Update1](./figures/Repeat-Update1.png)
88
89Now, **Repeat** notifies the parent component to re-lay out the nodes and compares the keys one by one. If the template ID of the node is the same as that of the original one, the note is reused to update the **key**, **index** and **data**. Otherwise, the node in the cache pool with the same template ID is reused to update the **key**, **index**, and **data**.
90
91![Repeat-Update2](./figures/Repeat-Update2.png)
92
93As shown in the preceding figure, node13 updates **data** and **index**; node14 updates the template ID and **index** and reuses a node from the cache pool; node15 reuses its own node and updates the **key**, **index**, and **data** synchronously because of the changed **key** and the unchanged template ID; node 16 and node 17 only update the **index**. The **index=17** node is new and reused from the cache pool.
94
95![Repeat-Update-Done](./figures/Repeat-Update-Done.png)
96
97## totalCount
98
99Total length of the data source, which can be greater than the number of loaded data items. Define the data source length as **arr.length**. The processing rules of **totalCount** are as follows:
100
101- When **totalCount** is set to the default value or a non-natural number, the value of **totalCount** is **arr.length**, and the list scrolls normally.
102- When **0** <= **totalCount** < **arr.length**, only **totalCount** list items are rendered.
103- When **totalCount** is greater than **arr.length**, Repeat renders **totalCount** list items, and the scroll bar style changes based on the value of **totalCount**.
104
105> **Note:**
106>
107> If **totalCount** is less than **array.length**, when the parent component container is scrolling, the application needs to ensure that subsequent data is requested when the list is about to slide to the end of the data source. You need to fix the data request error (caused by, for example, network delay) until all data sources are loaded. Otherwise, the scrolling effect is abnormal.
108
109## cachedCount
110
111**cachedCount** indicates the maximum number of subnodes that can be cached in the **Repeat** cache pool of the current template. This parameter is valid only when **virtualScroll** is enabled.
112
113You need to understand the differences between **.cachedCount()** of the scrolling container component and **cachedCount()** of **Repeat**. Both are used to balance performance and memory, but their definitions are different.
114
115- **.cachedCount()** indicates the nodes that are located in the component tree and treated as invisible. Container components such as **List** or **Grid** render these nodes to achieve better performance. But **Repeat** treats these nodes as visible.
116- **cachedCount()** of **Repeat** indicates the nodes that are treated as invisible by **Repeat**. These nodes are idle and are temporarily stored in the framework. You can update these nodes as required to implement reuse.
117
118When **cachedCount** is set to the maximum number of nodes that may appear on the screen of the current template, **Repeat** can be reused as much as possible. However, when there is no node of the current template on the screen, the cache pool is not released and the application memory increases. You need to set the configuration based on the actual situation.
119
120- If the default value is used, the framework calculates the value of **cachedCount** for each template based on the number of nodes displayed on the screen and the number of preloaded nodes. If the number increases, the value of **cachedCount** increases accordingly. Note that the value of cachedCount does not decrease.
121- Explicitly specify **cachedCount**. It is recommended that the value be the same as the number of nodes on the screen. Yet, setting **cachedCount** to less than 2 is not advised. Doing so may lead to the creation of new nodes during rapid scrolling, which could result in performance degradation.
122
123## Use Scenarios
124
125### Non-virtualScroll
126
127#### Changing the Data Source
128
129```ts
130@Entry
131@ComponentV2
132struct Parent {
133  @Local simpleList: Array<string> = ['one', 'two', 'three'];
134
135  build() {
136    Row() {
137      Column() {
138        Text('Click to change the value of the third array item')
139          .fontSize(24)
140          .fontColor(Color.Red)
141          .onClick(() => {
142            this.simpleList[2] = 'new three';
143          })
144
145        Repeat<string>(this.simpleList)
146            .each((obj: RepeatItem<string>)=>{
147              ChildItem({ item: obj.item })
148                .margin({top: 20})
149            })
150            .key((item: string) => item)
151      }
152      .justifyContent(FlexAlign.Center)
153      .width('100%')
154      .height('100%')
155    }
156    .height('100%')
157    .backgroundColor(0xF1F3F5)
158  }
159}
160
161@ComponentV2
162struct ChildItem {
163  @Param @Require item: string;
164
165  build() {
166    Text(this.item)
167      .fontSize(30)
168  }
169}
170```
171
172![ForEach-Non-Initial-Render-Case-Effect](./figures/ForEach-Non-Initial-Render-Case-Effect.gif)
173
174The component of the third array item is reused when the array item is re-rendered, and only the data is refreshed.
175
176#### Changing the Index Value
177
178In the following example, when array items 1 and 2 are exchanged, if the key is as the same as the last one, **Repeat** reuses the previous component and updates only the data of the component that uses the **index** value.
179
180```ts
181@Entry
182@ComponentV2
183struct Parent {
184  @Local simpleList: Array<string> = ['one', 'two', 'three'];
185
186  build() {
187    Row() {
188      Column() {
189        Text ('Exchange array items 1 and 2')
190          .fontSize(24)
191          .fontColor(Color.Red)
192          .onClick(() => {
193            let temp: string = this.simpleList[2]
194            this.simpleList[2] = this.simpleList[1]
195            this.simpleList[1] = temp
196          })
197          .margin({bottom: 20})
198
199        Repeat<string>(this.simpleList)
200          .each((obj: RepeatItem<string>)=>{
201            Text("index: " + obj.index)
202              .fontSize(30)
203            ChildItem({ item: obj.item })
204              .margin({bottom: 20})
205          })
206          .key((item: string) => item)
207      }
208      .justifyContent(FlexAlign.Center)
209      .width('100%')
210      .height('100%')
211    }
212    .height('100%')
213    .backgroundColor(0xF1F3F5)
214  }
215}
216
217@ComponentV2
218struct ChildItem {
219  @Param @Require item: string;
220
221  build() {
222    Text(this.item)
223      .fontSize(30)
224  }
225}
226```
227
228![Repeat-Non-Initial-Render-Case-Exchange-Effect](./figures/Repeat-Non-Initial-Render-Case-Exchange-Effect.gif)
229
230### VirtualScroll
231
232This section describes the actual application scenarios of **Repeat** and the reuse of component nodes in the **virtualScroll** scenario. A large number of test scenarios can be derived based on reuse rules. This section only describes typical data changes.
233
234#### One template
235
236The following code designs typical data source operations in the **virtualScroll** scenario of the **Repeat** component, including **inserting, modifying, deleting, and exchanging data**. Select an index value from the drop-down list and click the corresponding button to change the data. You can click two data items in sequence to exchange them.
237
238```ts
239@ObservedV2
240class Repeat005Clazz {
241  @Trace message: string = '';
242
243  constructor(message: string) {
244    this.message = message;
245  }
246}
247
248@Entry
249@ComponentV2
250struct RepeatVirtualScroll {
251  @Local simpleList: Array<Repeat005Clazz> = [];
252  private exchange: number[] = [];
253  private counter: number = 0;
254  @Local selectOptions: SelectOption[] = [];
255  @Local selectIdx: number = 0;
256
257  @Monitor("simpleList")
258  reloadSelectOptions(): void {
259    this.selectOptions = [];
260    for (let i = 0; i < this.simpleList.length; ++i) {
261      this.selectOptions.push({ value: i.toString() });
262    }
263    if (this.selectIdx >= this.simpleList.length) {
264      this.selectIdx = this.simpleList.length - 1;
265    }
266  }
267
268  aboutToAppear(): void {
269    for (let i = 0; i < 100; i++) {
270      this.simpleList.push(new Repeat005Clazz(`item_${i}`));
271    }
272    this.reloadSelectOptions();
273  }
274
275  handleExchange(idx: number): void { // Click to exchange child components.
276    this.exchange.push(idx);
277    if (this.exchange.length === 2) {
278      let _a = this.exchange[0];
279      let _b = this.exchange[1];
280      let temp: Repeat005Clazz = this.simpleList[_a];
281      this.simpleList[_a] = this.simpleList[_b];
282      this.simpleList[_b] = temp;
283      this.exchange = [];
284    }
285  }
286
287  build() {
288    Column({ space: 10 }) {
289      Text('virtualScroll each()&template() 1t')
290        .fontSize(15)
291        .fontColor(Color.Gray)
292      Text('Select an index and press the button to update data.')
293        .fontSize(15)
294        .fontColor(Color.Gray)
295
296      Select(this.selectOptions)
297        .selected(this.selectIdx)
298        .value(this.selectIdx.toString())
299        .key('selectIdx')
300        .onSelect((index: number) => {
301          this.selectIdx = index;
302        })
303      Row({ space: 5 }) {
304        Button('Add No.' + this.selectIdx)
305          .onClick(() => {
306            this.simpleList.splice(this.selectIdx, 0, new Repeat005Clazz(`${this.counter++}_add_item`));
307            this.reloadSelectOptions();
308          })
309        Button('Modify No.' + this.selectIdx)
310          .onClick(() => {
311            this.simpleList.splice(this.selectIdx, 1, new Repeat005Clazz(`${this.counter++}_modify_item`));
312          })
313        Button('Del No.' + this.selectIdx)
314          .onClick(() => {
315            this.simpleList.splice(this.selectIdx, 1);
316            this.reloadSelectOptions();
317          })
318      }
319      Button('Update array length to 5.')
320        .onClick(() => {
321          this.simpleList = this.simpleList.slice(0, 5);
322          this.reloadSelectOptions();
323        })
324
325      Text('Click on two items to exchange.')
326        .fontSize(15)
327        .fontColor(Color.Gray)
328
329      List({ space: 10 }) {
330        Repeat<Repeat005Clazz>(this.simpleList)
331          .each((obj: RepeatItem<Repeat005Clazz>) => {
332            ListItem() {
333              Text(`[each] index${obj.index}: ${obj.item.message}`)
334                .fontSize(25)
335                .onClick(() => {
336                  this.handleExchange(obj.index);
337                })
338            }
339          })
340          .key((item: Repeat005Clazz, index: number) => {
341            return item.message;
342          })
343          .virtualScroll({ totalCount: this.simpleList.length })
344          .templateId(() => "a")
345          .template('a', (ri) => {
346            Text(`[a] index${ri.index}: ${ri.item.message}`)
347              .fontSize(25)
348              .onClick(() => {
349                this.handleExchange(ri.index);
350              })
351          }, { cachedCount: 3 })
352      }
353      .cachedCount(2)
354      .border({ width: 1 })
355      .width('95%')
356      .height('40%')
357    }
358    .justifyContent(FlexAlign.Center)
359    .width('100%')
360    .height('100%')
361  }
362}
363```
364The application list contains 100 **message** properties of the custom class **RepeatClazz**. The value of **cachedCount** of the **List** component is set to **2**, and the cache pool size of the template A is set to **3**. The application screen is shown as bellow.
365
366![Repeat-VirtualScroll-Demo](./figures/Repeat-VirtualScroll-Demo.gif)
367
368#### Multiple templates
369
370```
371@ObservedV2
372class Repeat006Clazz {
373  @Trace message: string = '';
374
375  constructor(message: string) {
376    this.message = message;
377  }
378}
379
380@Entry
381@ComponentV2
382struct RepeatVirtualScroll2T {
383  @Local simpleList: Array<Repeat006Clazz> = [];
384  private exchange: number[] = [];
385  private counter: number = 0;
386  @Local selectOptions: SelectOption[] = [];
387  @Local selectIdx: number = 0;
388
389  @Monitor("simpleList")
390  reloadSelectOptions(): void {
391    this.selectOptions = [];
392    for (let i = 0; i < this.simpleList.length; ++i) {
393      this.selectOptions.push({ value: i.toString() });
394    }
395    if (this.selectIdx >= this.simpleList.length) {
396      this.selectIdx = this.simpleList.length - 1;
397    }
398  }
399
400  aboutToAppear(): void {
401    for (let i = 0; i < 100; i++) {
402      this.simpleList.push(new Repeat006Clazz(`item_${i}`));
403    }
404    this.reloadSelectOptions();
405  }
406
407  handleExchange(idx: number): void { // Click to exchange child components.
408    this.exchange.push(idx);
409    if (this.exchange.length === 2) {
410      let _a = this.exchange[0];
411      let _b = this.exchange[1];
412      let temp: Repeat006Clazz = this.simpleList[_a];
413      this.simpleList[_a] = this.simpleList[_b];
414      this.simpleList[_b] = temp;
415      this.exchange = [];
416    }
417  }
418
419  build() {
420    Column({ space: 10 }) {
421      Text('virtualScroll each()&template() 2t')
422        .fontSize(15)
423        .fontColor(Color.Gray)
424      Text('Select an index and press the button to update data.')
425        .fontSize(15)
426        .fontColor(Color.Gray)
427
428      Select(this.selectOptions)
429        .selected(this.selectIdx)
430        .value(this.selectIdx.toString())
431        .key('selectIdx')
432        .onSelect((index: number) => {
433          this.selectIdx = index;
434        })
435      Row({ space: 5 }) {
436        Button('Add No.' + this.selectIdx)
437          .onClick(() => {
438            this.simpleList.splice(this.selectIdx, 0, new Repeat006Clazz(`${this.counter++}_add_item`));
439            this.reloadSelectOptions();
440          })
441        Button('Modify No.' + this.selectIdx)
442          .onClick(() => {
443            this.simpleList.splice(this.selectIdx, 1, new Repeat006Clazz(`${this.counter++}_modify_item`));
444          })
445        Button('Del No.' + this.selectIdx)
446          .onClick(() => {
447            this.simpleList.splice(this.selectIdx, 1);
448            this.reloadSelectOptions();
449          })
450      }
451      Button('Update array length to 5.')
452        .onClick(() => {
453          this.simpleList = this.simpleList.slice(0, 5);
454          this.reloadSelectOptions();
455        })
456
457      Text('Click on two items to exchange.')
458        .fontSize(15)
459        .fontColor(Color.Gray)
460
461      List({ space: 10 }) {
462        Repeat<Repeat006Clazz>(this.simpleList)
463          .each((obj: RepeatItem<Repeat006Clazz>) => {
464            ListItem() {
465              Text(`[each] index${obj.index}: ${obj.item.message}`)
466                .fontSize(25)
467                .onClick(() => {
468                  this.handleExchange(obj.index);
469                })
470            }
471          })
472          .key((item: Repeat006Clazz, index: number) => {
473            return item.message;
474          })
475          .virtualScroll({ totalCount: this.simpleList.length })
476          .templateId((item: Repeat006Clazz, index: number) => {
477            return (index % 2 === 0) ? 'odd' : 'even';
478          })
479          .template('odd', (ri) => {
480            Text(`[odd] index${ri.index}: ${ri.item.message}`)
481              .fontSize(25)
482              .fontColor(Color.Blue)
483              .onClick(() => {
484                this.handleExchange(ri.index);
485              })
486          }, { cachedCount: 3 })
487          .template('even', (ri) => {
488            Text(`[even] index${ri.index}: ${ri.item.message}`)
489              .fontSize(25)
490              .fontColor(Color.Green)
491              .onClick(() => {
492                this.handleExchange(ri.index);
493              })
494          }, { cachedCount: 1 })
495      }
496      .cachedCount(2)
497      .border({ width: 1 })
498      .width('95%')
499      .height('40%')
500    }
501    .justifyContent(FlexAlign.Center)
502    .width('100%')
503    .height('100%')
504  }
505}
506```
507
508![Repeat-VirtualScroll-2T-Demo](./figures/Repeat-VirtualScroll-2T-Demo.gif)
509
510### Using Repeat in a Nesting Manner
511
512Example:
513
514```ts
515// Repeat can be nested in other components.
516@Entry
517@ComponentV2
518struct RepeatNest {
519  @Local outerList: string[] = [];
520  @Local innerList: number[] = [];
521
522  aboutToAppear(): void {
523    for (let i = 0; i < 20; i++) {
524      this.outerList.push(i.toString());
525      this.innerList.push(i);
526    }
527  }
528
529  build() {
530    Column({ space: 20 }) {
531      Text('Using Repeat virtualScroll in a Nesting Manner')
532        .fontSize(15)
533        .fontColor(Color.Gray)
534      List() {
535        Repeat<string>(this.outerList)
536          .each((obj) => {
537            ListItem() {
538              Column() {
539                Text('outerList item: ' + obj.item)
540                  .fontSize(30)
541                List() {
542                  Repeat<number>(this.innerList)
543                    .each((subObj) => {
544                      ListItem() {
545                        Text('innerList item: ' + subObj.item)
546                          .fontSize(20)
547                      }
548                    })
549                    .key((item) => "innerList_" + item)
550                }
551                .width('80%')
552                .border({ width: 1 })
553                .backgroundColor(Color.Orange)
554              }
555              .height('30%')
556              .backgroundColor(Color.Pink)
557            }
558            .border({ width: 1 })
559          })
560          .key((item) => "outerList_" + item)
561      }
562      .width('80%')
563      .border({ width: 1 })
564    }
565    .justifyContent(FlexAlign.Center)
566    .width('90%')
567    .height('80%')
568  }
569}
570```
571
572The figure below shows the effect.
573
574![Repeat-Nest](./figures/Repeat-Nest.png)
575
576## Application Scenario of the Parent Container Component
577
578### Using Together with List
579
580Use **virtualScroll** of **Repeat** in the **List** container component. The following is an example:
581
582```ts
583class DemoListItemInfo {
584  name: string;
585  icon: Resource;
586
587  constructor(name: string, icon: Resource) {
588    this.name = name;
589    this.icon = icon;
590  }
591}
592
593@Entry
594@ComponentV2
595struct DemoList {
596  @Local videoList: Array<DemoListItemInfo> = [];
597
598  aboutToAppear(): void {
599    for (let i = 0; i < 10; i++) {
600      // app.media.listItem0, app.media.listItem1, and app.media.listItem2 are only examples. Replace them with the actual ones in use.
601      this.videoList.push(new DemoListItemInfo('Video' + i,
602        i % 3 == 0 ? $r("app.media.listItem0") :
603        i % 3 == 1 ? $r("app.media.listItem1") : $r("app.media.listItem2")));
604    }
605  }
606
607  @Builder
608  itemEnd(index: number) {
609    Button('Delete')
610      .backgroundColor(Color.Red)
611      .onClick(() => {
612        this.videoList.splice(index, 1);
613      })
614  }
615
616  build() {
617    Column({ space: 10 }) {
618      Text('List Contains the Repeat Component')
619        .fontSize(15)
620        .fontColor(Color.Gray)
621
622      List({ space: 5 }) {
623        Repeat<DemoListItemInfo>(this.videoList)
624          .each((obj: RepeatItem<DemoListItemInfo>) => {
625            ListItem() {
626              Column() {
627                Image(obj.item.icon)
628                  .width('80%')
629                  .margin(10)
630                Text(obj.item.name)
631                  .fontSize(20)
632              }
633            }
634            .swipeAction({
635              end: {
636                builder: () => {
637                  this.itemEnd(obj.index);
638                }
639              }
640            })
641            .onAppear(() => {
642              console.info('AceTag', obj.item.name);
643            })
644          })
645          .key((item: DemoListItemInfo) => item.name)
646          .virtualScroll()
647      }
648      .cachedCount(2)
649      .height('90%')
650      .border({ width: 1 })
651      .listDirection(Axis.Vertical)
652      .alignListItem(ListItemAlign.Center)
653      .divider({
654        strokeWidth: 1,
655        startMargin: 60,
656        endMargin: 60,
657        color: '#ffe9f0f0'
658      })
659
660      Row({ space: 10 }) {
661        Button('Delete No.1')
662          .onClick(() => {
663            this.videoList.splice(0, 1);
664          })
665        Button('Delete No.5')
666          .onClick(() => {
667            this.videoList.splice(4, 1);
668          })
669      }
670    }
671    .width('100%')
672    .height('100%')
673    .justifyContent(FlexAlign.Center)
674  }
675}
676```
677Swipe left and touch the **Delete** button, or touch the button at the bottom to delete the video widget.
678
679![Repeat-Demo-List](./figures/Repeat-Demo-List.gif)
680
681### Using Together with Grid
682
683Use **virtualScroll** of **Repeat** in the **Grid** container component. The following is an example:
684
685```ts
686class DemoGridItemInfo {
687  name: string;
688  icon: Resource;
689
690  constructor(name: string, icon: Resource) {
691    this.name = name;
692    this.icon = icon;
693  }
694}
695
696@Entry
697@ComponentV2
698struct DemoGrid {
699  @Local itemList: Array<DemoGridItemInfo> = [];
700  @Local isRefreshing: boolean = false;
701  private layoutOptions: GridLayoutOptions = {
702    regularSize: [1, 1],
703    irregularIndexes: [10]
704  }
705  private GridScroller: Scroller = new Scroller();
706  private num: number = 0;
707
708  aboutToAppear(): void {
709    for (let i = 0; i < 10; i++) {
710      // app.media.gridItem0, app.media.gridItem1, and app.media.gridItem2 are only examples. Replace them with the actual ones in use.
711      this.itemList.push(new DemoGridItemInfo('Video' + i,
712        i % 3 == 0 ? $r("app.media.gridItem0") :
713        i % 3 == 1 ? $r("app.media.gridItem1") : $r("app.media.gridItem2")));
714    }
715  }
716
717  build() {
718    Column({ space: 10 }) {
719      Text('Grid Contains the Repeat Component')
720        .fontSize(15)
721        .fontColor(Color.Gray)
722
723      Refresh({ refreshing: $$this.isRefreshing }) {
724        Grid(this.GridScroller, this.layoutOptions) {
725          Repeat<DemoGridItemInfo>(this.itemList)
726            .each((obj: RepeatItem<DemoGridItemInfo>) => {
727              if (obj.index === 10 ) {
728                GridItem() {
729                  Text('Last viewed here. Touch to refresh.')
730                    .fontSize(20)
731                }
732                .height(30)
733                .border({ width: 1 })
734                .onClick(() => {
735                  this.GridScroller.scrollToIndex(0);
736                  this.isRefreshing = true;
737                })
738                .onAppear(() => {
739                  console.info('AceTag', obj.item.name);
740                })
741              } else {
742                GridItem() {
743                  Column() {
744                    Image(obj.item.icon)
745                      .width('100%')
746                      .height(80)
747                      .objectFit(ImageFit.Cover)
748                      .borderRadius({ topLeft: 16, topRight: 16 })
749                    Text(obj.item.name)
750                      .fontSize(15)
751                      .height(20)
752                  }
753                }
754                .height(100)
755                .borderRadius(16)
756                .backgroundColor(Color.White)
757                .onAppear(() => {
758                  console.info('AceTag', obj.item.name);
759                })
760              }
761            })
762            .key((item: DemoGridItemInfo) => item.name)
763            .virtualScroll()
764        }
765        .columnsTemplate('repeat(auto-fit, 150)')
766        .cachedCount(4)
767        .rowsGap(15)
768        .columnsGap(10)
769        .height('100%')
770        .padding(10)
771        .backgroundColor('#F1F3F5')
772      }
773      .onRefreshing(() => {
774        setTimeout(() => {
775          this.itemList.splice(10, 1);
776          this.itemList.unshift(new DemoGridItemInfo('refresh', $r('app.media.gridItem0'))); // app.media.gridItem0 is only an example. Replace it with the actual one.
777          for (let i = 0; i < 10; i++) {
778            // app.media.gridItem0, app.media.gridItem1, and app.media.gridItem2 are only examples. Replace them with the actual ones.
779            this.itemList.unshift(new DemoGridItemInfo('New video' + this.num,
780              i % 3 == 0 ? $r("app.media.gridItem0") :
781              i % 3 == 1 ? $r("app.media.gridItem1") : $r("app.media.gridItem2")));
782            this.num++;
783          }
784          this.isRefreshing = false;
785        }, 1000);
786        console.info('AceTag', 'onRefreshing');
787      })
788      .refreshOffset(64)
789      .pullToRefresh(true)
790      .width('100%')
791      .height('85%')
792
793      Button('Refresh')
794        .onClick(() => {
795          this.GridScroller.scrollToIndex(0);
796          this.isRefreshing = true;
797        })
798    }
799    .width('100%')
800    .height('100%')
801    .justifyContent(FlexAlign.Center)
802  }
803}
804```
805Swipe down on the screen, touch the **Refresh** button, or touch **Last viewed here. Touch to refresh.** to load new videos.
806
807![Repeat-Demo-Grid](./figures/Repeat-Demo-Grid.gif)
808
809### Using Together with Swiper
810
811Use **virtualScroll** of **Repeat** in the **Swiper** container component. The following is an example:
812
813```ts
814const remotePictures: Array<string> = [
815  'https://www.example.com/xxx/0001.jpg', // Set the specific network image address.
816  'https://www.example.com/xxx/0002.jpg',
817  'https://www.example.com/xxx/0003.jpg',
818  'https://www.example.com/xxx/0004.jpg',
819  'https://www.example.com/xxx/0005.jpg',
820  'https://www.example.com/xxx/0006.jpg',
821  'https://www.example.com/xxx/0007.jpg',
822  'https://www.example.com/xxx/0008.jpg',
823  'https://www.example.com/xxx/0009.jpg',
824]
825
826@ObservedV2
827class DemoSwiperItemInfo {
828  id: string;
829  @Trace url: string = 'default';
830
831  constructor(id: string) {
832    this.id = id;
833  }
834}
835
836@Entry
837@ComponentV2
838struct DemoSwiper {
839  @Local pics: Array<DemoSwiperItemInfo> = [];
840
841  aboutToAppear(): void {
842    for (let i = 0; i < 9; i++) {
843      this.pics.push(new DemoSwiperItemInfo('pic' + i));
844    }
845    setTimeout(() => {
846      this.pics[0].url = remotePictures[0];
847    }, 1000);
848  }
849
850  build() {
851    Column() {
852      Text('Swiper Contains the Repeat Component')
853        .fontSize(15)
854        .fontColor(Color.Gray)
855
856      Stack() {
857        Text('Loading...')
858          .fontSize(15)
859          .fontColor(Color.Gray)
860        Swiper() {
861          Repeat(this.pics)
862            .each((obj: RepeatItem<DemoSwiperItemInfo>) => {
863              Image(obj.item.url)
864                .onAppear(() => {
865                  console.info('AceTag', obj.item.id);
866                })
867            })
868            .key((item: DemoSwiperItemInfo) => item.id)
869            .virtualScroll()
870        }
871        .cachedCount(9)
872        .height('50%')
873        .loop(false)
874        .indicator(true)
875        .onChange((index) => {
876          setTimeout(() => {
877            this.pics[index].url = remotePictures[index];
878          }, 1000);
879        })
880      }
881      .width('100%')
882      .height('100%')
883      .backgroundColor(Color.Black)
884    }
885  }
886}
887```
888Load the image 1s later to simulate the network latency.
889
890![Repeat-Demo-Swiper](./figures/Repeat-Demo-Swiper.gif)
891
892## FAQs
893
894### Ensure that the Position of the Scrollbar Remains Unchanged When the List Data Outside the Screen Changes
895
896Declare the **Repeat** component in the **List** component to implement the **key** generation logic and **each** logic (as shown in the following sample code). Click **insert** to insert an element before the first element displayed on the screen, enabling the screen to scroll down.
897
898```ts
899// Define a class and mark it as observable.
900// Customize an array in the class and mark it as traceable.
901@ObservedV2
902class ArrayHolder {
903  @Trace arr: Array<number> = [];
904
905  // constructor, used to initialize arrays.
906  constructor(count: number) {
907    for (let i = 0; i < count; i++) {
908      this.arr.push(i);
909    }
910  }
911}
912
913@Entry
914@ComponentV2
915export struct RepeatTemplateSingle {
916  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
917  @Local totalCount: number = this.arrayHolder.arr.length;
918  scroller: Scroller = new Scroller();
919
920  build() {
921    Column({ space: 5 }) {
922      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
923        Repeat(this.arrayHolder.arr)
924          .virtualScroll({ totalCount: this.totalCount })
925          .templateId((item, index) => {
926            return 'number';
927          })
928          .template('number', (r) => {
929            ListItem() {
930              Text(r.index! + ":" + r.item + "Reuse");
931            }
932          })
933          .each((r) => {
934            ListItem() {
935              Text(r.index! + ":" + r.item + "eachMessage");
936            }
937          })
938      }
939      .height('30%')
940
941      Button(`insert totalCount ${this.totalCount}`)
942        .height(60)
943        .onClick(() => {
944          // Insert an element which locates in the previous position displayed on the screen.
945          this.arrayHolder.arr.splice(18, 0, this.totalCount);
946          this.totalCount = this.arrayHolder.arr.length;
947        })
948    }
949    .width('100%')
950    .margin({ top: 5 })
951  }
952}
953```
954
955The figure below shows the effect.
956
957![Repeat-case1-Error](./figures/Repeat-Case1-Error.gif)
958
959In some scenarios, if you do not want the data source change outside the screen to affect the position where the **Scroller** of the **List** stays on the screen, you can use the [onScrollIndex](https://gitee.com/openharmony/docs/blob/master/en/application-dev/ui/arkts-layout-development-create-list.md#responding-to-the-scrolling-position) of the **List** component to listen for the scrolling action. When the list scrolls, you can obtain the scrolling position of a list. Use the [scrollToIndex](https://gitee.com/openharmony/docs/blob/OpenHarmony-5.0.2-Release/en/application-dev/reference/apis-arkui/arkui-ts/ts-container-scroll.md#scrolltoindex) feature of the **Scroller** component to slide to the specified **index** position. In this way, when data is added to or deleted from the data source outside the screen, the position where the **Scroller** stays remains unchanged.
960
961The following code shows the case of adding data to the data source.
962
963```ts
964// The definition of ArrayHolder is the same as that in the demo code.
965
966@Entry
967@ComponentV2
968export struct RepeatTemplateSingle {
969  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
970  @Local totalCount: number = this.arrayHolder.arr.length;
971  scroller: Scroller = new Scroller();
972
973  private start: number = 1;
974  private end: number = 1;
975
976  build() {
977    Column({ space: 5 }) {
978      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
979        Repeat(this.arrayHolder.arr)
980          .virtualScroll({ totalCount: this.totalCount })
981          .templateId((item, index) => {
982            return 'number';
983          })
984          .template('number', (r) => {
985            ListItem() {
986              Text(r.index! + ":" + r.item + "Reuse");
987            }
988          })
989          .each((r) => {
990            ListItem() {
991              Text(r.index! + ":" + r.item + "eachMessage");
992            }
993          })
994      }
995      .onScrollIndex((start, end) => {
996        this.start = start;
997        this.end = end;
998      })
999      .height('30%')
1000
1001      Button(`insert totalCount ${this.totalCount}`)
1002        .height(60)
1003        .onClick(() => {
1004          // Insert an element which locates in the previous position displayed on the screen.
1005          this.arrayHolder.arr.splice(18, 0, this.totalCount);
1006          let rect = this.scroller.getItemRect(this.start); // Obtain the size and position of the child component.
1007          this.scroller.scrollToIndex(this.start + 1); // Slide to the specified index.
1008          this.scroller.scrollBy(0, -rect.y); // Slide by a specified distance.
1009          this.totalCount = this.arrayHolder.arr.length;
1010        })
1011    }
1012    .width('100%')
1013    .margin({ top: 5 })
1014  }
1015}
1016```
1017
1018The figure below shows the effect.
1019
1020![Repeat-case1-Succ](./figures/Repeat-Case1-Succ.gif)
1021
1022### The totalCount Value Is Greater Than the Length of Data Source
1023
1024When the total length of the data source is large, the lazy loading is used to load some data first. To enable **Repeat** to display the correct scrollbar style, you need to change the value of **totalCount** to the total length of data. That is, before all data sources are loaded, the value of **totalCount** is greater than that of **array.length**.
1025
1026If **totalCount** is larger than **array.length**, when the parent component container is scrolling, the application needs to ensure that subsequent data is requested when the list is about to slide to the end of the data source. You need to fix the data request error (caused by, for example, network delay) until all data sources are loaded. Otherwise, the scrolling effect is abnormal.
1027
1028You can use the callback of [onScrollIndex](https://gitee.com/openharmony/docs/blob/master/en/application-dev/ui/arkts-layout-development-create-list.md#controlling-the-scrolling-position) attribute of the **List** or **Grid** parent component to implement the preceding specification. The sample code is as follows:
1029
1030```ts
1031@ObservedV2
1032class VehicleData {
1033  @Trace name: string;
1034  @Trace price: number;
1035
1036  constructor(name: string, price: number) {
1037    this.name = name;
1038    this.price = price;
1039  }
1040}
1041
1042@ObservedV2
1043class VehicleDB {
1044  public vehicleItems: VehicleData[] = [];
1045
1046  constructor() {
1047    // init data size 20
1048    for (let i = 1; i <= 20; i++) {
1049      this.vehicleItems.push(new VehicleData(`Vehicle${i}`, i));
1050    }
1051  }
1052}
1053
1054@Entry
1055@ComponentV2
1056struct entryCompSucc {
1057  @Local vehicleItems: VehicleData[] = new VehicleDB().vehicleItems;
1058  @Local listChildrenSize: ChildrenMainSize = new ChildrenMainSize(60);
1059  @Local totalCount: number = this.vehicleItems.length;
1060  scroller: Scroller = new Scroller();
1061
1062  build() {
1063    Column({ space: 3 }) {
1064      List({ scroller: this.scroller }) {
1065        Repeat(this.vehicleItems)
1066          .virtualScroll({ totalCount: 50 }) // total data size 50
1067          .templateId(() => 'default')
1068          .template('default', (ri) => {
1069            ListItem() {
1070              Column() {
1071                Text(`${ri.item.name} + ${ri.index}`)
1072                  .width('90%')
1073                  .height(this.listChildrenSize.childDefaultSize)
1074                  .backgroundColor(0xFFA07A)
1075                  .textAlign(TextAlign.Center)
1076                  .fontSize(20)
1077                  .fontWeight(FontWeight.Bold)
1078              }
1079            }.border({ width: 1 })
1080          }, { cachedCount: 5 })
1081          .each((ri) => {
1082            ListItem() {
1083              Text("Wrong: " + `${ri.item.name} + ${ri.index}`)
1084                .width('90%')
1085                .height(this.listChildrenSize.childDefaultSize)
1086                .backgroundColor(0xFFA07A)
1087                .textAlign(TextAlign.Center)
1088                .fontSize(20)
1089                .fontWeight(FontWeight.Bold)
1090            }.border({ width: 1 })
1091          })
1092          .key((item, index) => `${index}:${item}`)
1093      }
1094      .height('50%')
1095      .margin({ top: 20 })
1096      .childrenMainSize(this.listChildrenSize)
1097      .alignListItem(ListItemAlign.Center)
1098      .onScrollIndex((start, end) => {
1099        console.log('onScrollIndex', start, end);
1100        // lazy data loading
1101        if (this.vehicleItems.length < 50) {
1102          for (let i = 0; i < 10; i++) {
1103            if (this.vehicleItems.length < 50) {
1104              this.vehicleItems.push(new VehicleData("Vehicle_loaded", i));
1105            }
1106          }
1107        }
1108      })
1109    }
1110  }
1111}
1112```
1113
1114The figure below shows the effect.
1115
1116![Repeat-Case2-Succ](./figures/Repeat-Case2-Succ.gif)
1117
1118### Constraints on the Mixed Use of Repeat and @Builder
1119
1120When **Repeat** and @Builder are used together, parameters of the **RepeatItem** type must be passed so that the component can listen for data changes. If only **RepeatItem.item** or **RepeatItem.index** is passed, UI rendering exceptions occur.
1121
1122The sample code is as follows:
1123
1124```ts
1125@Entry
1126@ComponentV2
1127struct RepeatBuilderPage {
1128  @Local simpleList1: Array<number> = [];
1129  @Local simpleList2: Array<number> = [];
1130
1131  aboutToAppear(): void {
1132    for (let i = 0; i < 100; i++) {
1133      this.simpleList1.push(i)
1134      this.simpleList2.push(i)
1135    }
1136  }
1137
1138  build() {
1139    Column({ space: 20 }) {
1140      Text('Use Repeat and @Builder together: The abnormal display is on the left, and the normal display is on the right.')
1141        .fontSize(15)
1142        .fontColor(Color.Gray)
1143
1144      Row({ space: 20 }) {
1145        List({ initialIndex: 5, space: 20 }) {
1146          Repeat<number>(this.simpleList1)
1147            .each((ri) => {})
1148            .virtualScroll({ totalCount: this.simpleList1.length })
1149            .templateId((item: number, index: number) => "default")
1150            .template('default', (ri) => {
1151              ListItem() {
1152                Column() {
1153                  Text('Text id = ' + ri.item)
1154                    .fontSize(20)
1155                  this.buildItem1 (ri.item) // Change to this.buildItem1(ri).
1156                }
1157              }
1158              .border({ width: 1 })
1159            }, { cachedCount: 3 })
1160        }
1161        .cachedCount(1)
1162        .border({ width: 1 })
1163        .width('45%')
1164        .height('60%')
1165
1166        List({ initialIndex: 5, space: 20 }) {
1167          Repeat<number>(this.simpleList2)
1168            .each((ri) => {})
1169            .virtualScroll({ totalCount: this.simpleList2.length })
1170            .templateId((item: number, index: number) => "default")
1171            .template('default', (ri) => {
1172              ListItem() {
1173                Column() {
1174                  Text('Text id = ' + ri.item)
1175                    .fontSize(20)
1176                  this.buildItem2(ri)
1177                }
1178              }
1179              .border({ width: 1 })
1180            }, { cachedCount: 3 })
1181        }
1182        .cachedCount(1)
1183        .border({ width: 1 })
1184        .width('45%')
1185        .height('60%')
1186      }
1187    }
1188    .height('100%')
1189    .justifyContent(FlexAlign.Center)
1190  }
1191
1192  @Builder
1193  // The @Builder parameter must be of the RepeatItem type for normal rendering.
1194  buildItem1(item: number) {
1195    Text('Builder1 id = ' + item)
1196      .fontSize(20)
1197      .fontColor(Color.Red)
1198      .margin({ top: 2 })
1199  }
1200
1201  @Builder
1202  buildItem2(ri: RepeatItem<number>) {
1203    Text('Builder2 id = ' + ri.item)
1204      .fontSize(20)
1205      .fontColor(Color.Red)
1206      .margin({ top: 2 })
1207  }
1208}
1209```
1210
1211The following figure shows the display effect. Swipe down the list and you can see the difference. The incorrect usage is on the left, and the correct usage is on the right. (The **Text** component is in black and the **Builder** component is in red). The preceding code shows the error-prone scenario during development. That is, only the value, instead the entire **RepeatItem** class, is passed in the @Builder function.
1212
1213![Repeat-Builder](./figures/Repeat-Builder.png)
1214