• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1# Repeat: Reusable Repeated Rendering
2
3> **NOTE**
4>
5> **Repeat** is supported since API version 12.
6>
7> This topic is a developer guide. For details about API parameters, see [Repeat](https://gitee.com/openharmony/docs/blob/master/en/application-dev/reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md).
8
9## Overview
10
11**Repeat** is used to perform repeated rendering based on array data. Generally, it is used together with container components. The **Repeat** component supports two modes:
12
13- Non-virtualScroll: All child components in the list are loaded during page initialization. This mode applies to scenarios where all short data lists or components are loaded. For details, see [Non-virtualscroll](#non-virtualscroll).
14- virtualScroll: (For details about how to enable virtualScroll, see [virtualScroll](../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#virtualscroll)) The child components are loaded based on the valid loading area (including visible area and preload area) of the container components. When the container slides or the array changes, **Repeat** recalculates the valid loading range based on the parameters passed by the parent container component and manages the creation and destruction of list nodes in real time.
15This mode applies to scenarios where long data lists need to be lazy loaded or performance needs to be optimized through component reuse. For details, see [virtualScroll](#virtualscroll).
16
17> **NOTE**
18>
19> The differences between **Repeat**, **ForEach**, and **LazyForEach** are as follows:
20>
21> - Compared with [ForEach](arkts-rendering-control-foreach.md), the non-virtualScroll mode optimizes the rendering performance in specific array updates and manages the content and index of child components at the framework layer.
22> - Compared with [LazyForEach](arkts-rendering-control-lazyforeach.md), the virtualScroll mode directly listens to the changes of state variables. However, **LazyForEach** requires you to implement the [IDataSource](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md#idatasource10) API to manually manage the modification of the content and index of the child component. In addition, Repeat enhances the node reuse capability and improves the rendering performance for long list sliding and data update. The template function is added to **Repeat**. In the same array, different child components are rendered based on the custom template type.
23
24The following sample code uses the virtualScroll mode for repeated rendering.
25
26```ts
27// Use the virtualScroll mode in the List container component.
28@Entry
29@ComponentV2 // The decorator of V2 is recommended.
30struct RepeatExample {
31  @Local dataArr: Array<string> = []; // Data source
32
33  aboutToAppear(): void {
34    for (let i = 0; i < 50; i++) {
35      this.dataArr.push(`data_${i}`); // Add data to the array.
36    }
37  }
38
39  build() {
40    Column() {
41      List() {
42        Repeat<string>(this.dataArr)
43          .each((ri: RepeatItem<string>) => { // Default template
44            ListItem() {
45              Text('each_A_' + ri.item).fontSize(30).fontColor(Color.Red) // The text color is red.
46            }
47          })
48          .key((item: string, index: number): string => item) // Key generator.
49          .virtualScroll({ totalCount: this.dataArr.length }) // Enable the virtualScroll mode. totalCount indicates the data length to be loaded.
50          .templateId((item: string, index: number): string => { // Search for the corresponding template child component for rendering based on the return value.
51            return index <= 4 ? 'A' : (index <= 10 ? 'B' : ''); // The first five node templates are A, the next five node templates are B, and the rest are default templates.
52          })
53          .template('A', (ri: RepeatItem<string>) => { // Template A
54            ListItem() {
55              Text('ttype_A_' + ri.item).fontSize(30).fontColor(Color.Green) // The text color is green.
56            }
57          }, { cachedCount: 3 }) // The cache list capacity of template A is 3.
58          .template('B', (ri: RepeatItem<string>) => { // Template B
59            ListItem() {
60              Text('ttype_B_' + ri.item).fontSize(30).fontColor(Color.Blue) // The text color is blue.
61            }
62          }, { cachedCount: 4 }) // The cache list capacity of template B is 4.
63      }
64      .cachedCount(2) // Size of the preload area of the container component
65      .height('70%')
66      .border({ width: 1 }) // Border
67    }
68  }
69}
70```
71
72Execute the sample code, and you will see the following screen:
73
74![Repeat-NonVS-KeyGen](./figures/Repeat-Example.png)
75
76## Constraints
77
78- Generally, **Repeat** is used together with the container component and the child component is allowed to be contained in the container component. For example, when **Repeat** is used together with the [List](../reference/apis-arkui/arkui-ts/ts-container-list.md) component, the child component must be the [ListItem](../reference/apis-arkui/arkui-ts/ts-container-listitem.md) component.
79- When **Repeat** is used together with a custom component or the [@Builder function](./arkts-builder.md), 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, the UI rendering is abnormal. For details, see [Constraints on the Mixed Use of Repeat and @Builder](#constraints-on-the-mixed-use-of-repeat-and-builder).
80
81Constraints on using the virtualScroll mode:
82
83- This mode 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 the virtualScroll mode.
84- Decorators of V1 are not supported. If this mode is used together with the decorators of V1, the rendering is abnormal.
85- Only one child component can be created. The generated child component must be allowed to be contained in the **Repeat** parent container component.
86- The scrolling container component can contain only one **Repeat**. 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.
87- If the value of **totalCount** is greater than the 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).
88
89**Repeat** uses keys to identify which data is added or deleted, and which data changes its position (index). You are advised to use **.key()** as follows:
90
91- Even if the array changes, you must ensure that the key is unique.
92- Each time **.key()** is executed, the same data item is used as the input, and the output must be consistent.
93- (Not recommended) Use index in **.key()**. When the data item is moved, the index changes, and the key changes accordingly. As a result, **Repeat** considers that the data changes and triggers the child component to be rendered again, which deteriorates the performance.
94- (Recommended) Convert a simple array to a class object array, add the **readonly id** property, and assign a unique value to it in the constructor.
95
96Since API version 18, you are not advised to use **.key()**. However, if you use **.key()** according to the preceding suggestions, **Repeat** can still maintain its compatibility and performance.
97
98> **NOTE**
99>
100> The **Repeat** child component node can be created, updated, reused, and destroyed. A difference between node update and node reuse is as follows:
101>
102> - Node update: Nodes are not detached from the component tree. Only state variables are updated.
103> - Node reuse: Old nodes are detached from the component tree and stored in the idle node cache pool. New nodes obtain reusable nodes from the cache pool and are attached to the tree again.
104
105## Non-virtual scroll
106
107### Key Generation Rules
108
109The following figure shows the logic of **.key()**.
110
111If **.key()** is not specified, **Repeat** generates a new random key. If a duplicate key is found, **Repeat** recursively generates a key based on the existing key until no duplicate key exists.
112
113![Repeat-NonVS-KeyGen](./figures/Repeat-NonVS-KeyGen.png)
114
115### Child Component Rendering Logic
116
117When **Repeat** is rendered for the first time, all child components are created. After the array is changed, Repeat performs the following operations:
118
119First, traverse the old array keys. If the key does not exist in the new array, add it to the key set **deletedKeys**.
120
121Second, traverse the new array keys and perform the corresponding operation when any of the following conditions is met:
122
1231. If the same key can be found in the old array, the corresponding child component node is directly used and the index is updated.
1242. If **deletedKeys** is not empty, update nodes corresponding to the keys in the set according to the last in first out (LIFO) policy.
1253. If **deletedKeys** is empty, that is, no node can be updated. In this case, create a node.
126
127Third, if **deletedKeys** is not empty after the new array keys are traversed, the nodes corresponding to the keys in the set are destroyed.
128
129![Repeat-NonVS-FuncGen](./figures/Repeat-NonVS-FuncGen.png)
130
131The following figure shows an example of array changes.
132
133![Repeat-NonVS-Example](./figures/Repeat-NonVS-Example.png)
134
135According to the preceding logic, **item_0** does not change, **item_1** and **item_2** only update indexes, **item_n1** and **item_n2** are obtained by updating **item_4** and **item_3**, respectively, and **item_n3** is the created node.
136
137## virtualScroll
138
139### Key Generation Rules
140
141Since API version 18:
142
143If you do not define **.key()**, **Repeat** directly compares the array data changes to determine whether the child nodes are changed. (If yes, the page refresh logic is triggered.) If duplicate keys exist, **Repeat** generates a random key as the key of the current data item. Note that each time the page is refreshed, **.key()** is recalculated (that is, duplicate keys are generated again) to further generate a new random key. The format of a random key is **___${index}_+_${key}_+_${Math.random()}**, in which the variables are index, old key, and random number.
144
145### Child Component Rendering Logic
146
147When **Repeat** is rendered for the first time, the required child components are created based on the valid loading area (including visible area and preload area) of the container component.
148
149When the container slides or the array changes, the invalid child component nodes (which are out of the valid loading area) are added to the idle node cache list (that is, detached from the component tree without destruction). When a new component needs to be generated, reuse the components in the cache (the variable values of the reused child components are updated and attached to the tree again). Since API version 18, **Repeat** supports [custom component freezing in virtualScroll mode](./arkts-custom-components-freezeV2.md#repeat-virtualscroll).
150
151By default, the reuse function is enabled for **Repeat** in virtualScroll mode. You can configure the **reusable** field to determine whether to enable the reuse function since API version 18. To improve rendering performance, you are advised to enable the reuse function. For details about the sample code, see [VirtualScrollOptions](../reference/apis-arkui/arkui-ts/ts-rendering-control-repeat.md#virtualscrolloptions).
152
153The following uses [sliding scenario](#sliding-scenario) and [data update scenario](#data-update-scenario) to show the rendering logic of the child component in virtualScroll mode. Define an array with a length of 20. The template type for the first five items in the array is **aa** and for the others are **bb**. The capacity of the buffer pool **aa** is 3 and that of **bb** is 4. The size of the preload area of the container component is 2. For easy understanding, one and two idle nodes are added to the cache pools **aa** and **bb** respectively.
154
155The following figure shows the node status in the list during initial rendering.
156
157![Repeat-Start](./figures/Repeat-Start.png)
158
159#### Sliding Scenario
160
161Swipe the screen to the right for a distance of one node and **Repeat** starts to reuse the nodes in the cache pool. The node whose index is 10 enters the valid loading area. Its template type is **bb**. Because the cache pool **bb** is not empty, **Repeat** obtains an idle node from this pool for reuse and updates the node attributes. Other grandchild components related to the data item and index in the child component are updated synchronously based on the rules of state management of V2. The rest nodes are still in the valid loading area and only their indexes are updated.
162
163The node whose index is 0 is out of the valid loading area. When the UI main thread is idle, the system checks whether the cache pool **aa** is full. If it is not full, the system adds the node to the corresponding cache pool;
164
165otherwise, **Repeat** destroys redundant nodes.
166
167![Repeat-Slide](./figures/Repeat-Slide.png)
168
169#### Data Update Scenario
170
171Perform the following array update operations based on the previous section. Delete the node whose index is 4 and change **item_7** to **new_7**.
172
173After the node whose index is 4 is deleted, this invalid node is added to the cache pool **aa**. The subsequent nodes move leftwards. The **item_11** node that enters the valid loading area reuses the idle node in the cache pool **bb**. For other nodes, only the index is updated, as shown in the following figure.
174
175![Repeat-Update1](./figures/Repeat-Update1.png)
176
177Then, the **item_5** node move leftwards and its index is updated to 4. According to the calculation rule, the **item_5** node changes its template type to **aa**, reuses an idle node from the cache pool **aa**, and adds the old node to the cache pool **bb**, as shown in the following figure.
178
179![Repeat-Update2](./figures/Repeat-Update2.png)
180
181### template: Child Component Rendering Template
182
183Currently, the template can be used only in virtualScroll mode.
184
185- Each node obtains the template type based on **.templateId()** and renders the child component in the corresponding **.template()**.
186- If multiple template types are the same, **Repeat** overwrites the previously defined **.template()** and only the last defined **.template()** takes effect.
187- If the corresponding template type cannot be found, the child component in **.template()** whose type is empty is rendered first. If the child component does not exist, the child component in **.each()** is rendered.
188
189### cachedCount: Size of the Idle Node Cache List
190
191**cachedCount** indicates the maximum number of child components that can be cached in the cache pool of the corresponding template type. This parameter is valid only in virtualScroll mode.
192
193> **NOTE**
194>
195> The **.cachedCount()** attribute of the scrolling container component and the **cachedCount** parameter of the **.template()** attribute of **Repeat** are used to balance performance and memory, but their meanings are different.
196> - **.cachedCount()** indicates the nodes that are attached to the component tree and treated as invisible. Container components such as **List** or **Grid** render these nodes to achieve better performance. However, **Repeat** treats these nodes as visible.
197> - **cachedCount** in **.template()** 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.
198
199When **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.
200
201- 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.
202- 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.
203
204### totalCount: Length of the Data to Be Loaded
205
206**totalCount** indicates the length of the data to be loaded. The default value is the length of the original array. The value 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:
207
208- 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.
209- When **totalCount** is greater that or equal to **0** and smaller than **arr.length**, only data within the range of [0, *totalCount* - 1] is rendered.
210- When **totalCount** is greater than **arr.length**, data in the range of [0, *totalCount* - 1] will be rendered. The scrollbar style changes based on the value of **totalCount**.
211
212> **NOTE**
213>
214> If **totalCount** is greater than **arr.length**, the application should request subsequent data when the list scrolls 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.
215
216### onTotalCount: Calculating the Expected Data Length
217
218onTotalCount?(): number;
219
220It is supported since API version 18 and must be used in virtualScroll mode. You can customize a method to calculate the expected array length. The return value must be a natural number and may not be equal to the actual data source length **arr.length**. The processing rules of **onTotalCount** are as follows:
221
222- When the return value is a non-natural number, **arr.length** is used as the return value and the list scrolls normally.
223- When the return value of **onTotalCount** is greater that or equal to **0** and smaller than **arr.length**, only data within the range of [0, *return value* - 1] is rendered.
224- When the return value of **onTotalCount** is greater than **arr.length**, the data within the range of [0, *return value* - 1] is rendered. The scrollbar style changes based on the return value of **onTotalCount**.
225
226> **NOTE**
227>
228> - Compared with using **totalCount**, **Repeat** can proactively call the **onTotalCount** method to update the expected data length when necessary.
229> - Either **totalCount** or **onTotalCount** can be set. If neither of them is set, the default **arr.length** is used. If both of them are set, **totalCount** is ignored.
230> - When the return value of **onTotalCount** is greater than **arr.length**, you are advised to use **onLazyLoading** to implement lazy loading.
231
232### onLazyLoading: Precise Lazy Loading
233
234onLazyLoading?(index: number): void;
235
236It is supported since API version 18 and must be used in virtualScroll mode. You can customize a method to write data to a specified index in the data source. The processing rules of **onLazyLoading** are as follows:
237
238- Before reading the data corresponding to an index in the data source, **Repeat** checks whether the index contains data.
239- If no data exists and a custom method is defined, **Repeat** calls this method.
240- In the **onLazyLoading** method, data should be written to the index specified by **Repeat** in the format of **arr[index] = ...**. In addition, array operations except **[]** are not allowed, and elements except the specified index cannot be written. Otherwise, the system throws an exception.
241- After the **onLazyLoading** method is executed, if no data exists in the specified index, the rendering is abnormal.
242
243> **NOTE**
244>
245> - When using **onLazyLoading**, you are advised to use **onTotalCount** together instead of **totalCount**.
246> - If the expected data source length is greater than the actual one, **onLazyLoading** is recommended.
247> - Avoid using the **onLazyLoading** method to execute time-consuming operations. If data loading takes a long time, you can create a placeholder for the data in the **onLazyLoading** method and then create an asynchronous task to load the data.
248> - When **onLazyLoading** is used and **onTotalCount** is set to **arr.length + 1**, data can be loaded infinitely. In this scenario, you need to provide the initial data required for the first screen display and set **cachedCount** that is greater than 0 for the parent container component. Otherwise, the rendering is abnormal. If the **onLazyLoading** method is used together with the loop mode of **Swipe**, the **onLazyLoading** method will be triggered continuously when screen stays at the node whose index is 0 Therefore, you are advised not to use them together. In addition, you need to pay attention to the memory usage to avoid excessive memory consumption caused by continuous data loading.
249
250## Use Scenarios
251
252### Non-virtualScroll
253
254#### Changing the Data Source
255
256```ts
257@Entry
258@ComponentV2
259struct Parent {
260  @Local simpleList: Array<string> = ['one', 'two', 'three'];
261
262  build() {
263    Row() {
264      Column() {
265        Text('Click to change the value of the third array item')
266          .fontSize(24)
267          .fontColor(Color.Red)
268          .onClick(() => {
269            this.simpleList[2] = 'new three';
270          })
271
272        Repeat<string>(this.simpleList)
273            .each((obj: RepeatItem<string>)=>{
274              ChildItem({ item: obj.item })
275                .margin({top: 20})
276            })
277            .key((item: string) => item)
278      }
279      .justifyContent(FlexAlign.Center)
280      .width('100%')
281      .height('100%')
282    }
283    .height('100%')
284    .backgroundColor(0xF1F3F5)
285  }
286}
287
288@ComponentV2
289struct ChildItem {
290  @Param @Require item: string;
291
292  build() {
293    Text(this.item)
294      .fontSize(30)
295  }
296}
297```
298
299![ForEach-Non-Initial-Render-Case-Effect](./figures/ForEach-Non-Initial-Render-Case-Effect.gif)
300
301The component of the third array item is reused when the array item is re-rendered, and only the data is refreshed.
302
303#### Changing the Index Value
304
305In 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.
306
307```ts
308@Entry
309@ComponentV2
310struct Parent {
311  @Local simpleList: Array<string> = ['one', 'two', 'three'];
312
313  build() {
314    Row() {
315      Column() {
316        Text('Exchange array items 1 and 2')
317          .fontSize(24)
318          .fontColor(Color.Red)
319          .onClick(() => {
320            let temp: string = this.simpleList[2]
321            this.simpleList[2] = this.simpleList[1]
322            this.simpleList[1] = temp
323          })
324          .margin({bottom: 20})
325
326        Repeat<string>(this.simpleList)
327          .each((obj: RepeatItem<string>)=>{
328            Text("index: " + obj.index)
329              .fontSize(30)
330            ChildItem({ item: obj.item })
331              .margin({bottom: 20})
332          })
333          .key((item: string) => item)
334      }
335      .justifyContent(FlexAlign.Center)
336      .width('100%')
337      .height('100%')
338    }
339    .height('100%')
340    .backgroundColor(0xF1F3F5)
341  }
342}
343
344@ComponentV2
345struct ChildItem {
346  @Param @Require item: string;
347
348  build() {
349    Text(this.item)
350      .fontSize(30)
351  }
352}
353```
354
355![Repeat-Non-Initial-Render-Case-Exchange-Effect](./figures/Repeat-Non-Initial-Render-Case-Exchange-Effect.gif)
356
357### VirtualScroll
358
359This section describes the actual use scenarios of **Repeat** and the reuse of component nodes in virtualScroll mode. A large number of test scenarios can be derived based on reuse rules. This section only describes typical data changes.
360
361#### One Template
362
363The following sample code shows how to insert, modify, delete, and exchange data in an array in virtualScroll mode. 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.
364
365```ts
366@ObservedV2
367class Repeat005Clazz {
368  @Trace message: string = '';
369
370  constructor(message: string) {
371    this.message = message;
372  }
373}
374
375@Entry
376@ComponentV2
377struct RepeatVirtualScroll {
378  @Local simpleList: Array<Repeat005Clazz> = [];
379  private exchange: number[] = [];
380  private counter: number = 0;
381  @Local selectOptions: SelectOption[] = [];
382  @Local selectIdx: number = 0;
383
384  @Monitor("simpleList")
385  reloadSelectOptions(): void {
386    this.selectOptions = [];
387    for (let i = 0; i < this.simpleList.length; ++i) {
388      this.selectOptions.push({ value: i.toString() });
389    }
390    if (this.selectIdx >= this.simpleList.length) {
391      this.selectIdx = this.simpleList.length - 1;
392    }
393  }
394
395  aboutToAppear(): void {
396    for (let i = 0; i < 100; i++) {
397      this.simpleList.push(new Repeat005Clazz(`item_${i}`));
398    }
399    this.reloadSelectOptions();
400  }
401
402  handleExchange(idx: number): void { // Click to exchange child components.
403    this.exchange.push(idx);
404    if (this.exchange.length === 2) {
405      let _a = this.exchange[0];
406      let _b = this.exchange[1];
407      let temp: Repeat005Clazz = this.simpleList[_a];
408      this.simpleList[_a] = this.simpleList[_b];
409      this.simpleList[_b] = temp;
410      this.exchange = [];
411    }
412  }
413
414  build() {
415    Column({ space: 10 }) {
416      Text('virtualScroll each()&template() 1t')
417        .fontSize(15)
418        .fontColor(Color.Gray)
419      Text('Select an index and press the button to update data.')
420        .fontSize(15)
421        .fontColor(Color.Gray)
422
423      Select(this.selectOptions)
424        .selected(this.selectIdx)
425        .value(this.selectIdx.toString())
426        .key('selectIdx')
427        .onSelect((index: number) => {
428          this.selectIdx = index;
429        })
430      Row({ space: 5 }) {
431        Button('Add No.' + this.selectIdx)
432          .onClick(() => {
433            this.simpleList.splice(this.selectIdx, 0, new Repeat005Clazz(`${this.counter++}_add_item`));
434            this.reloadSelectOptions();
435          })
436        Button('Modify No.' + this.selectIdx)
437          .onClick(() => {
438            this.simpleList.splice(this.selectIdx, 1, new Repeat005Clazz(`${this.counter++}_modify_item`));
439          })
440        Button('Del No.' + this.selectIdx)
441          .onClick(() => {
442            this.simpleList.splice(this.selectIdx, 1);
443            this.reloadSelectOptions();
444          })
445      }
446      Button('Update array length to 5.')
447        .onClick(() => {
448          this.simpleList = this.simpleList.slice(0, 5);
449          this.reloadSelectOptions();
450        })
451
452      Text('Click on two items to exchange.')
453        .fontSize(15)
454        .fontColor(Color.Gray)
455
456      List({ space: 10 }) {
457        Repeat<Repeat005Clazz>(this.simpleList)
458          .each((obj: RepeatItem<Repeat005Clazz>) => {
459            ListItem() {
460              Text(`[each] index${obj.index}: ${obj.item.message}`)
461                .fontSize(25)
462                .onClick(() => {
463                  this.handleExchange(obj.index);
464                })
465            }
466          })
467          .key((item: Repeat005Clazz, index: number) => {
468            return item.message;
469          })
470          .virtualScroll({ totalCount: this.simpleList.length })
471          .templateId(() => "a")
472          .template('a', (ri) => {
473            Text(`[a] index${ri.index}: ${ri.item.message}`)
474              .fontSize(25)
475              .onClick(() => {
476                this.handleExchange(ri.index);
477              })
478          }, { cachedCount: 3 })
479      }
480      .cachedCount(2)
481      .border({ width: 1 })
482      .width('95%')
483      .height('40%')
484    }
485    .justifyContent(FlexAlign.Center)
486    .width('100%')
487    .height('100%')
488  }
489}
490```
491The 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.
492
493![Repeat-VirtualScroll-Demo](./figures/Repeat-VirtualScroll-Demo.gif)
494
495#### Multiple Templates
496
497```ts
498@ObservedV2
499class Repeat006Clazz {
500  @Trace message: string = '';
501
502  constructor(message: string) {
503    this.message = message;
504  }
505}
506
507@Entry
508@ComponentV2
509struct RepeatVirtualScroll2T {
510  @Local simpleList: Array<Repeat006Clazz> = [];
511  private exchange: number[] = [];
512  private counter: number = 0;
513  @Local selectOptions: SelectOption[] = [];
514  @Local selectIdx: number = 0;
515
516  @Monitor("simpleList")
517  reloadSelectOptions(): void {
518    this.selectOptions = [];
519    for (let i = 0; i < this.simpleList.length; ++i) {
520      this.selectOptions.push({ value: i.toString() });
521    }
522    if (this.selectIdx >= this.simpleList.length) {
523      this.selectIdx = this.simpleList.length - 1;
524    }
525  }
526
527  aboutToAppear(): void {
528    for (let i = 0; i < 100; i++) {
529      this.simpleList.push(new Repeat006Clazz(`item_${i}`));
530    }
531    this.reloadSelectOptions();
532  }
533
534  handleExchange(idx: number): void { // Click to exchange child components.
535    this.exchange.push(idx);
536    if (this.exchange.length === 2) {
537      let _a = this.exchange[0];
538      let _b = this.exchange[1];
539      let temp: Repeat006Clazz = this.simpleList[_a];
540      this.simpleList[_a] = this.simpleList[_b];
541      this.simpleList[_b] = temp;
542      this.exchange = [];
543    }
544  }
545
546  build() {
547    Column({ space: 10 }) {
548      Text('virtualScroll each()&template() 2t')
549        .fontSize(15)
550        .fontColor(Color.Gray)
551      Text('Select an index and press the button to update data.')
552        .fontSize(15)
553        .fontColor(Color.Gray)
554
555      Select(this.selectOptions)
556        .selected(this.selectIdx)
557        .value(this.selectIdx.toString())
558        .key('selectIdx')
559        .onSelect((index: number) => {
560          this.selectIdx = index;
561        })
562      Row({ space: 5 }) {
563        Button('Add No.' + this.selectIdx)
564          .onClick(() => {
565            this.simpleList.splice(this.selectIdx, 0, new Repeat006Clazz(`${this.counter++}_add_item`));
566            this.reloadSelectOptions();
567          })
568        Button('Modify No.' + this.selectIdx)
569          .onClick(() => {
570            this.simpleList.splice(this.selectIdx, 1, new Repeat006Clazz(`${this.counter++}_modify_item`));
571          })
572        Button('Del No.' + this.selectIdx)
573          .onClick(() => {
574            this.simpleList.splice(this.selectIdx, 1);
575            this.reloadSelectOptions();
576          })
577      }
578      Button('Update array length to 5.')
579        .onClick(() => {
580          this.simpleList = this.simpleList.slice(0, 5);
581          this.reloadSelectOptions();
582        })
583
584      Text('Click on two items to exchange.')
585        .fontSize(15)
586        .fontColor(Color.Gray)
587
588      List({ space: 10 }) {
589        Repeat<Repeat006Clazz>(this.simpleList)
590          .each((obj: RepeatItem<Repeat006Clazz>) => {
591            ListItem() {
592              Text(`[each] index${obj.index}: ${obj.item.message}`)
593                .fontSize(25)
594                .onClick(() => {
595                  this.handleExchange(obj.index);
596                })
597            }
598          })
599          .key((item: Repeat006Clazz, index: number) => {
600            return item.message;
601          })
602          .virtualScroll({ totalCount: this.simpleList.length })
603          .templateId((item: Repeat006Clazz, index: number) => {
604            return (index % 2 === 0) ? 'odd' : 'even';
605          })
606          .template('odd', (ri) => {
607            Text(`[odd] index${ri.index}: ${ri.item.message}`)
608              .fontSize(25)
609              .fontColor(Color.Blue)
610              .onClick(() => {
611                this.handleExchange(ri.index);
612              })
613          }, { cachedCount: 3 })
614          .template('even', (ri) => {
615            Text(`[even] index${ri.index}: ${ri.item.message}`)
616              .fontSize(25)
617              .fontColor(Color.Green)
618              .onClick(() => {
619                this.handleExchange(ri.index);
620              })
621          }, { cachedCount: 1 })
622      }
623      .cachedCount(2)
624      .border({ width: 1 })
625      .width('95%')
626      .height('40%')
627    }
628    .justifyContent(FlexAlign.Center)
629    .width('100%')
630    .height('100%')
631  }
632}
633```
634
635![Repeat-VirtualScroll-2T-Demo](./figures/Repeat-VirtualScroll-2T-Demo.gif)
636
637#### Precise Lazy Loading
638
639If the total length of a data source or data item loading duration is long, you can use lazy loading to prevent all data from being loaded during initialization.
640
641**Example 1**
642
643The total length of the data source is long. When the data is rendered for the first time, the screen is scrolled, or the display area is switched, the data in the corresponding area is dynamically loaded.
644
645```ts
646@Entry
647@ComponentV2
648struct RepeatLazyLoading {
649  // Assume that the total length of the data source is 1000. The initial array does not provide data.
650  @Local arr: Array<string> = [];
651  scroller: Scroller = new Scroller();
652  build() {
653    Column({ space: 5 }) {
654      // The initial index displayed on the screen is 100. The data can be automatically obtained through lazy loading.
655      List({ scroller: this.scroller, space: 5, initialIndex: 100 }) {
656        Repeat(this.arr)
657          .virtualScroll({
658            // The expected total length of the data source is 1000.
659            onTotalCount: () => { return 1000; },
660            // Implement lazy loading.
661            onLazyLoading: (index: number) => { this.arr[index] = index.toString(); }
662          })
663          .each((obj: RepeatItem<string>) => {
664            ListItem() {
665              Row({ space: 5 }) {
666                Text(`${obj.index}: Item_${obj.item}`)
667              }
668            }
669            .height(50)
670          })
671      }
672      .height('80%')
673      .border({ width: 1})
674      // Redirect to the index whose value is 500. The data can be automatically obtained through lazy loading.
675      Button('ScrollToIndex 500')
676        .onClick(() => { this.scroller.scrollToIndex(500); })
677    }
678  }
679}
680```
681
682The figure below shows the effect.
683
684![Repeat-Lazyloading-1](./figures/repeat-lazyloading-demo1.gif)
685
686**Example 2**
687
688Data loading takes a long time. In the **onLazyLoading** method, placeholders are created for data items, and then data is loaded through asynchronous tasks.
689
690```ts
691@Entry
692@ComponentV2
693struct RepeatLazyLoading {
694  @Local arr: Array<string> = [];
695  build() {
696    Column({ space: 5 }) {
697      List({ space: 5 }) {
698        Repeat(this.arr)
699          .virtualScroll({
700            onTotalCount: () => { return 100; },
701            // Implement lazy loading.
702            onLazyLoading: (index: number) => {
703              // Create a placeholder.
704              this.arr[index] = '';
705              // Simulate a time-consuming loading process and load data through an asynchronous task.
706              setTimeout(() => { this.arr[index] = index.toString(); }, 1000);
707            }
708          })
709          .each((obj: RepeatItem<string>) => {
710            ListItem() {
711              Row({ space: 5 }) {
712                Text(`${obj.index}: Item_${obj.item}`)
713              }
714            }
715            .height(50)
716          })
717      }
718      .height('100%')
719      .border({ width: 1})
720    }
721  }
722}
723```
724
725The figure below shows the effect.
726
727![Repeat-Lazyloading-2](./figures/repeat-lazyloading-demo2.gif)
728
729**Example 3**
730
731Lazy loading is used together with **onTotalCount: () => { return this.arr.length + 1; }** to implement unlimited lazy loading.
732
733> **NOTE**
734>
735> - In this scenario, you need to provide the initial data required for the first screen display and set **cachedCount** that is greater than 0 for the parent container component. Otherwise, the rendering is abnormal.
736> - If the **onLazyLoading** method is used together with the loop mode of **Swipe**, the **onLazyLoading** method will be triggered continuously when screen stays at the node whose index is 0 Therefore, you are advised not to use them together.
737> - You need to pay attention to the memory usage to avoid excessive memory consumption caused by continuous data loading.
738
739```ts
740@Entry
741@ComponentV2
742struct RepeatLazyLoading {
743  @Local arr: Array<string> = [];
744  // Provide the initial data required for the first screen display.
745  aboutToAppear(): void {
746    for (let i = 0; i < 15; i++) {
747      this.arr.push(i.toString());
748    }
749  }
750  build() {
751    Column({ space: 5 }) {
752      List({ space: 5 }) {
753        Repeat(this.arr)
754          .virtualScroll({
755            // Unlimited lazy loading.
756            onTotalCount: () => { return this.arr.length + 1; },
757            onLazyLoading: (index: number) => { this.arr[index] = index.toString(); }
758          })
759          .each((obj: RepeatItem<string>) => {
760            ListItem() {
761              Row({ space: 5 }) {
762                Text(`${obj.index}: Item_${obj.item}`)
763              }
764            }
765            .height(50)
766          })
767      }
768      .height('100%')
769      .border({ width: 1})
770      // You are advised to set cachedCount to a value greater than 0.
771      .cachedCount(1)
772    }
773  }
774}
775```
776
777The figure below shows the effect.
778
779![Repeat-Lazyloading-3](./figures/repeat-lazyloading-demo3.gif)
780
781### Using Repeat in a Nesting Manner
782
783**Repeat** can be nested in other components. The following showcases the sample code for nesting **Repeat** in virtualScroll mode:
784
785```ts
786// Repeat can be nested in other components.
787@Entry
788@ComponentV2
789struct RepeatNest {
790  @Local outerList: string[] = [];
791  @Local innerList: number[] = [];
792
793  aboutToAppear(): void {
794    for (let i = 0; i < 20; i++) {
795      this.outerList.push(i.toString());
796      this.innerList.push(i);
797    }
798  }
799
800  build() {
801    Column({ space: 20 }) {
802      Text('Using Repeat virtualScroll in a Nesting Manner')
803        .fontSize(15)
804        .fontColor(Color.Gray)
805      List() {
806        Repeat<string>(this.outerList)
807          .each((obj) => {
808            ListItem() {
809              Column() {
810                Text('outerList item: ' + obj.item)
811                  .fontSize(30)
812                List() {
813                  Repeat<number>(this.innerList)
814                    .each((subObj) => {
815                      ListItem() {
816                        Text('innerList item: ' + subObj.item)
817                          .fontSize(20)
818                      }
819                    })
820                    .key((item) => "innerList_" + item)
821                    .virtualScroll()
822                }
823                .width('80%')
824                .border({ width: 1 })
825                .backgroundColor(Color.Orange)
826              }
827              .height('30%')
828              .backgroundColor(Color.Pink)
829            }
830            .border({ width: 1 })
831          })
832          .key((item) => "outerList_" + item)
833          .virtualScroll()
834      }
835      .width('80%')
836      .border({ width: 1 })
837    }
838    .justifyContent(FlexAlign.Center)
839    .width('90%')
840    .height('80%')
841  }
842}
843```
844
845The figure below shows the effect.
846
847![Repeat-Nest](./figures/Repeat-Nest.png)
848
849### Use Scenarios of the Parent Container Component
850
851This section describes the common use scenarios of virtualScroll mode and container components.
852
853#### Using Together with List
854
855Use virtualScroll mode of **Repeat** in the **List** container component. The following is an example:
856
857```ts
858class DemoListItemInfo {
859  name: string;
860  icon: Resource;
861
862  constructor(name: string, icon: Resource) {
863    this.name = name;
864    this.icon = icon;
865  }
866}
867
868@Entry
869@ComponentV2
870struct DemoList {
871  @Local videoList: Array<DemoListItemInfo> = [];
872
873  aboutToAppear(): void {
874    for (let i = 0; i < 10; i++) {
875      // app.media.listItem0, app.media.listItem1, and app.media.listItem2 are only examples. Replace them with the actual ones in use.
876      this.videoList.push(new DemoListItemInfo('Video' + i,
877        i % 3 == 0 ? $r("app.media.listItem0") :
878        i % 3 == 1 ? $r("app.media.listItem1") : $r("app.media.listItem2")));
879    }
880  }
881
882  @Builder
883  itemEnd(index: number) {
884    Button('Delete')
885      .backgroundColor(Color.Red)
886      .onClick(() => {
887        this.videoList.splice(index, 1);
888      })
889  }
890
891  build() {
892    Column({ space: 10 }) {
893      Text('List Contains the Repeat Component')
894        .fontSize(15)
895        .fontColor(Color.Gray)
896
897      List({ space: 5 }) {
898        Repeat<DemoListItemInfo>(this.videoList)
899          .each((obj: RepeatItem<DemoListItemInfo>) => {
900            ListItem() {
901              Column() {
902                Image(obj.item.icon)
903                  .width('80%')
904                  .margin(10)
905                Text(obj.item.name)
906                  .fontSize(20)
907              }
908            }
909            .swipeAction({
910              end: {
911                builder: () => {
912                  this.itemEnd(obj.index);
913                }
914              }
915            })
916            .onAppear(() => {
917              console.info('AceTag', obj.item.name);
918            })
919          })
920          .key((item: DemoListItemInfo) => item.name)
921          .virtualScroll()
922      }
923      .cachedCount(2)
924      .height('90%')
925      .border({ width: 1 })
926      .listDirection(Axis.Vertical)
927      .alignListItem(ListItemAlign.Center)
928      .divider({
929        strokeWidth: 1,
930        startMargin: 60,
931        endMargin: 60,
932        color: '#ffe9f0f0'
933      })
934
935      Row({ space: 10 }) {
936        Button('Delete No.1')
937          .onClick(() => {
938            this.videoList.splice(0, 1);
939          })
940        Button('Delete No.5')
941          .onClick(() => {
942            this.videoList.splice(4, 1);
943          })
944      }
945    }
946    .width('100%')
947    .height('100%')
948    .justifyContent(FlexAlign.Center)
949  }
950}
951```
952
953Swipe left and touch the **Delete** button, or touch the button at the bottom to delete the video widget.
954
955![Repeat-Demo-List](./figures/Repeat-Demo-List.gif)
956
957#### Using Together with Grid
958
959Use **virtualScroll** of **Repeat** in the **Grid** container component. The following is an example:
960
961```ts
962class DemoGridItemInfo {
963  name: string;
964  icon: Resource;
965
966  constructor(name: string, icon: Resource) {
967    this.name = name;
968    this.icon = icon;
969  }
970}
971
972@Entry
973@ComponentV2
974struct DemoGrid {
975  @Local itemList: Array<DemoGridItemInfo> = [];
976  @Local isRefreshing: boolean = false;
977  private layoutOptions: GridLayoutOptions = {
978    regularSize: [1, 1],
979    irregularIndexes: [10]
980  }
981  private GridScroller: Scroller = new Scroller();
982  private num: number = 0;
983
984  aboutToAppear(): void {
985    for (let i = 0; i < 10; i++) {
986      // app.media.gridItem0, app.media.gridItem1, and app.media.gridItem2 are only examples. Replace them with the actual ones in use.
987      this.itemList.push(new DemoGridItemInfo('Video' + i,
988        i % 3 == 0 ? $r("app.media.gridItem0") :
989        i % 3 == 1 ? $r("app.media.gridItem1") : $r("app.media.gridItem2")));
990    }
991  }
992
993  build() {
994    Column({ space: 10 }) {
995      Text('Grid Contains the Repeat Component')
996        .fontSize(15)
997        .fontColor(Color.Gray)
998
999      Refresh({ refreshing: $$this.isRefreshing }) {
1000        Grid(this.GridScroller, this.layoutOptions) {
1001          Repeat<DemoGridItemInfo>(this.itemList)
1002            .each((obj: RepeatItem<DemoGridItemInfo>) => {
1003              if (obj.index === 10 ) {
1004                GridItem() {
1005                  Text('Last viewed here. Touch to refresh.')
1006                    .fontSize(20)
1007                }
1008                .height(30)
1009                .border({ width: 1 })
1010                .onClick(() => {
1011                  this.GridScroller.scrollToIndex(0);
1012                  this.isRefreshing = true;
1013                })
1014                .onAppear(() => {
1015                  console.info('AceTag', obj.item.name);
1016                })
1017              } else {
1018                GridItem() {
1019                  Column() {
1020                    Image(obj.item.icon)
1021                      .width('100%')
1022                      .height(80)
1023                      .objectFit(ImageFit.Cover)
1024                      .borderRadius({ topLeft: 16, topRight: 16 })
1025                    Text(obj.item.name)
1026                      .fontSize(15)
1027                      .height(20)
1028                  }
1029                }
1030                .height(100)
1031                .borderRadius(16)
1032                .backgroundColor(Color.White)
1033                .onAppear(() => {
1034                  console.info('AceTag', obj.item.name);
1035                })
1036              }
1037            })
1038            .key((item: DemoGridItemInfo) => item.name)
1039            .virtualScroll()
1040        }
1041        .columnsTemplate('repeat(auto-fit, 150)')
1042        .cachedCount(4)
1043        .rowsGap(15)
1044        .columnsGap(10)
1045        .height('100%')
1046        .padding(10)
1047        .backgroundColor('#F1F3F5')
1048      }
1049      .onRefreshing(() => {
1050        setTimeout(() => {
1051          this.itemList.splice(10, 1);
1052          this.itemList.unshift(new DemoGridItemInfo('refresh', $r('app.media.gridItem0'))); // app.media.gridItem0 is only an example. Replace it with the actual one.
1053          for (let i = 0; i < 10; i++) {
1054            // app.media.gridItem0, app.media.gridItem1, and app.media.gridItem2 are only examples. Replace them with the actual ones.
1055            this.itemList.unshift(new DemoGridItemInfo('New video' + this.num,
1056              i % 3 == 0 ? $r("app.media.gridItem0") :
1057              i % 3 == 1 ? $r("app.media.gridItem1") : $r("app.media.gridItem2")));
1058            this.num++;
1059          }
1060          this.isRefreshing = false;
1061        }, 1000);
1062        console.info('AceTag', 'onRefreshing');
1063      })
1064      .refreshOffset(64)
1065      .pullToRefresh(true)
1066      .width('100%')
1067      .height('85%')
1068
1069      Button('Refresh')
1070        .onClick(() => {
1071          this.GridScroller.scrollToIndex(0);
1072          this.isRefreshing = true;
1073        })
1074    }
1075    .width('100%')
1076    .height('100%')
1077    .justifyContent(FlexAlign.Center)
1078  }
1079}
1080```
1081
1082Swipe down on the screen, touch the **Refresh** button, or touch **Last viewed here. Touch to refresh.** to load new videos.
1083
1084![Repeat-Demo-Grid](./figures/Repeat-Demo-Grid.gif)
1085
1086#### Using Together with Swiper
1087
1088Use **virtualScroll** of **Repeat** in the **Swiper** container component. The following is an example:
1089
1090```ts
1091const remotePictures: Array<string> = [
1092  'https://www.example.com/xxx/0001.jpg', // Set the specific network image address.
1093  'https://www.example.com/xxx/0002.jpg',
1094  'https://www.example.com/xxx/0003.jpg',
1095  'https://www.example.com/xxx/0004.jpg',
1096  'https://www.example.com/xxx/0005.jpg',
1097  'https://www.example.com/xxx/0006.jpg',
1098  'https://www.example.com/xxx/0007.jpg',
1099  'https://www.example.com/xxx/0008.jpg',
1100  'https://www.example.com/xxx/0009.jpg'
1101];
1102
1103@ObservedV2
1104class DemoSwiperItemInfo {
1105  id: string;
1106  @Trace url: string = 'default';
1107
1108  constructor(id: string) {
1109    this.id = id;
1110  }
1111}
1112
1113@Entry
1114@ComponentV2
1115struct DemoSwiper {
1116  @Local pics: Array<DemoSwiperItemInfo> = [];
1117
1118  aboutToAppear(): void {
1119    for (let i = 0; i < 9; i++) {
1120      this.pics.push(new DemoSwiperItemInfo('pic' + i));
1121    }
1122    setTimeout(() => {
1123      this.pics[0].url = remotePictures[0];
1124    }, 1000);
1125  }
1126
1127  build() {
1128    Column() {
1129      Text('Swiper Contains the Repeat Component')
1130        .fontSize(15)
1131        .fontColor(Color.Gray)
1132
1133      Stack() {
1134        Text('Loading...')
1135          .fontSize(15)
1136          .fontColor(Color.Gray)
1137        Swiper() {
1138          Repeat(this.pics)
1139            .each((obj: RepeatItem<DemoSwiperItemInfo>) => {
1140              Image(obj.item.url)
1141                .onAppear(() => {
1142                  console.info('AceTag', obj.item.id);
1143                })
1144            })
1145            .key((item: DemoSwiperItemInfo) => item.id)
1146            .virtualScroll()
1147        }
1148        .cachedCount(9)
1149        .height('50%')
1150        .loop(false)
1151        .indicator(true)
1152        .onChange((index) => {
1153          setTimeout(() => {
1154            this.pics[index].url = remotePictures[index];
1155          }, 1000);
1156        })
1157      }
1158      .width('100%')
1159      .height('100%')
1160      .backgroundColor(Color.Black)
1161    }
1162  }
1163}
1164```
1165
1166Load the image 1s later to simulate the network latency.
1167
1168![Repeat-Demo-Swiper](./figures/Repeat-Demo-Swiper.gif)
1169
1170### Enabling Drag and Sort
1171
1172If **Repeat** is used in a list, and the **onMove** event is set, you can enable drag and sort for the list items. Both the non-virtualScroll and virtualScroll modes support drag and sort.
1173
1174#### Constraints
1175- If an item changes the position after you drag and sort the data, the **onMove** event is triggered to report the original index and target index of the item. The data source needs to be modified in the **onMove** event based on the reported start index and target index. Before and after the data source is modified, the value of each item must remain unchanged to ensure that the drop animation can be executed properly.
1176- During the drag and sort, the data source cannot be modified.
1177
1178#### Sample Code
1179```ts
1180@Entry
1181@ComponentV2
1182struct RepeatVirtualScrollOnMove {
1183  @Local simpleList: Array<string> = [];
1184
1185  aboutToAppear(): void {
1186    for (let i = 0; i < 100; i++) {
1187      this.simpleList.push(`${i}`);
1188    }
1189  }
1190
1191  build() {
1192    Column() {
1193      List() {
1194        Repeat<string>(this.simpleList)
1195          // Set onMove to enable the drag and sort.
1196          .onMove((from: number, to: number) => {
1197            let temp = this.simpleList.splice(from, 1);
1198            this.simpleList.splice(to, 0, temp[0]);
1199          })
1200          .each((obj: RepeatItem<string>) => {
1201            ListItem() {
1202              Text(obj.item)
1203                .fontSize(16)
1204                .textAlign(TextAlign.Center)
1205                .size({height: 100, width: "100%"})
1206            }.margin(10)
1207            .borderRadius(10)
1208            .backgroundColor("#FFFFFFFF")
1209          })
1210          .key((item: string, index: number) => {
1211            return item;
1212          })
1213          .virtualScroll({ totalCount: this.simpleList.length })
1214      }
1215      .border({ width: 1 })
1216      .backgroundColor("#FFDCDCDC")
1217      .width('100%')
1218      .height('100%')
1219    }
1220  }
1221}
1222```
1223
1224The figure below shows the effect.
1225
1226![Repeat-Drag-Sort](figures/ForEach-Drag-Sort.gif)
1227
1228### Using .key() to Control the Node Refresh Range
1229
1230Since API version 18, when you customize **.key()**, the child nodes of **Repeat** determine whether to update themselves based on the key. After the array is modified: (1) If the key is changed, the page is refreshed immediately and the data is updated to the new value. (2) If the key is not changed, the page is not refreshed.
1231
1232Prerequisites: The array is rendered in virtualScroll mode. The data item **RepeatData** is a class decorated by @ObservedV2. Two properties of this class, **id** and **msg**, are decorated by @Trace. The value of **msg** is used as the content of the node rendered in the list. Click the **click** button to modify the content of the first node in the list.
1233
1234Scenario 1: When the property value of the list node data changes, the page is refreshed, and the data of the first list node is updated to the modified value.
1235
1236This scenario can be implemented in either of the following ways: (1) Define **.key()** and change the key value of the corresponding node. (2) If **.key()** is not defined, **Repeat** directly checks whether the data object is changed. The sample code is as follows:
1237
1238```ts
1239@ObservedV2
1240class RepeatData {
1241  @Trace id: string;
1242  @Trace msg: string;
1243
1244  constructor(id: string, msg: string) {
1245    this.id = id;
1246    this.msg = msg;
1247  }
1248}
1249
1250@Entry
1251@ComponentV2
1252struct RepeatRerender {
1253  @Local dataArr: Array<RepeatData> = [];
1254
1255  aboutToAppear(): void {
1256    for (let i = 0; i < 10; i++) {
1257      this.dataArr.push(new RepeatData(`key${i}`, `data${i}`));
1258    }
1259  }
1260
1261  build() {
1262    Column({ space: 20 }) {
1263      List() {
1264        Repeat<RepeatData>(this.dataArr)
1265          .each((ri: RepeatItem<RepeatData>) => {
1266            ListItem() {
1267              Text(ri.item.msg).fontSize(30)
1268            }
1269          })
1270          .key((item: RepeatData, index: number) => item.msg) // Method 1: Set the return value of .key() to be consistent with the value of changed node data, for example, the value of msg.
1271          // Method 2: Delete .key().
1272          .virtualScroll()
1273      }
1274      .cachedCount(2)
1275      .width('100%')
1276      .height('40%')
1277      .border({ width: 1 })
1278      .backgroundColor(0xFAEEE0)
1279
1280      Button('click').onClick(() => {
1281        this.dataArr.splice(0, 1, new RepeatData('key0', 'new msg')); // Change the value of msg of the first node.
1282      })
1283    }
1284  }
1285}
1286```
1287
1288After you click the button, the data changes as follows.
1289
1290![Repeat-Rerender-Wrong](./figures/Repeat-Rerender-Wrong.gif)
1291
1292Scenario 2: When the property value of the list node data changes but the key remains unchanged, page refresh is not triggered immediately, so that a node refresh frequency is controlled and overall rendering performance of the page is improved.
1293
1294Implementation: Define **.key()**. The return value is the **id** property of the node data object. After you click the button, the value of **id** (key) remains unchanged. After you change the value of **msg**, the page is not refreshed. The sample code is as follows:
1295
1296Note that if you directly modify the **msg** property (**this.dataArr[0].msg ='new msg'**), the page is still refreshed. This is because the value of **msg** is decorated by @Trace. If the value is directly modified, the change logic of state variable is triggered and the page is refreshed immediately.
1297
1298```ts
1299@ObservedV2
1300class RepeatData {
1301  @Trace id: string;
1302  @Trace msg: string;
1303
1304  constructor(id: string, msg: string) {
1305    this.id = id;
1306    this.msg = msg;
1307  }
1308}
1309
1310@Entry
1311@ComponentV2
1312struct RepeatRerender {
1313  @Local dataArr: Array<RepeatData> = [];
1314
1315  aboutToAppear(): void {
1316    for (let i = 0; i < 10; i++) {
1317      this.dataArr.push(new RepeatData(`key${i}`, `data${i}`));
1318    }
1319  }
1320
1321  build() {
1322    Column({ space: 20 }) {
1323      List() {
1324        Repeat<RepeatData>(this.dataArr)
1325          .each((ri: RepeatItem<RepeatData>) => {
1326            ListItem() {
1327              Text(ri.item.msg).fontSize(30)
1328            }
1329          })
1330          .key((item: RepeatData, index: number) => item.id) // Set the return value of .key() to a value that is not affected by the change of child node, for example, the value of id.
1331          .virtualScroll()
1332      }
1333      .cachedCount(2)
1334      .width('100%')
1335      .height('40%')
1336      .border({ width: 1 })
1337      .backgroundColor(0xFAEEE0)
1338
1339      Button('click').onClick(() => {
1340        this.dataArr.splice(0, 1, new RepeatData('key0', 'new msg')); // Change the value of msg of the first node data and retain the value of id.
1341      })
1342    }
1343  }
1344}
1345```
1346
1347After you click the button, the data does not change.
1348
1349![Repeat-Rerender-Correct](./figures/Repeat-Rerender-Correct.gif)
1350
1351## FAQs
1352
1353### Ensure that the Position of the Scrollbar Remains Unchanged When the List Data Outside the Screen Changes
1354
1355Declare 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.
1356
1357```ts
1358// Define a class and mark it as observable.
1359// Customize an array in the class and mark it as traceable.
1360@ObservedV2
1361class ArrayHolder {
1362  @Trace arr: Array<number> = [];
1363
1364  // constructor, used to initialize arrays.
1365  constructor(count: number) {
1366    for (let i = 0; i < count; i++) {
1367      this.arr.push(i);
1368    }
1369  }
1370}
1371
1372@Entry
1373@ComponentV2
1374struct RepeatTemplateSingle {
1375  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
1376  @Local totalCount: number = this.arrayHolder.arr.length;
1377  scroller: Scroller = new Scroller();
1378
1379  build() {
1380    Column({ space: 5 }) {
1381      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
1382        Repeat(this.arrayHolder.arr)
1383          .virtualScroll({ totalCount: this.totalCount })
1384          .templateId((item, index) => {
1385            return 'number';
1386          })
1387          .template('number', (r) => {
1388            ListItem() {
1389              Text(r.index! + ":" + r.item + "Reuse");
1390            }
1391          })
1392          .each((r) => {
1393            ListItem() {
1394              Text(r.index! + ":" + r.item + "eachMessage");
1395            }
1396          })
1397      }
1398      .height('30%')
1399
1400      Button(`insert totalCount ${this.totalCount}`)
1401        .height(60)
1402        .onClick(() => {
1403          // Insert an element which locates in the previous position displayed on the screen.
1404          this.arrayHolder.arr.splice(18, 0, this.totalCount);
1405          this.totalCount = this.arrayHolder.arr.length;
1406        })
1407    }
1408    .width('100%')
1409    .margin({ top: 5 })
1410  }
1411}
1412```
1413
1414The figure below shows the effect.
1415
1416![Repeat-case1-Error](./figures/Repeat-Case1-Error.gif)
1417
1418In 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/master/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.
1419
1420The following code shows the case of adding data to the data source.
1421
1422```ts
1423// The definition of ArrayHolder is the same as that in the demo code.
1424
1425@Entry
1426@ComponentV2
1427struct RepeatTemplateSingle {
1428  @Local arrayHolder: ArrayHolder = new ArrayHolder(100);
1429  @Local totalCount: number = this.arrayHolder.arr.length;
1430  scroller: Scroller = new Scroller();
1431
1432  private start: number = 1;
1433  private end: number = 1;
1434
1435  build() {
1436    Column({ space: 5 }) {
1437      List({ space: 20, initialIndex: 19, scroller: this.scroller }) {
1438        Repeat(this.arrayHolder.arr)
1439          .virtualScroll({ totalCount: this.totalCount })
1440          .templateId((item, index) => {
1441            return 'number';
1442          })
1443          .template('number', (r) => {
1444            ListItem() {
1445              Text(r.index! + ":" + r.item + "Reuse");
1446            }
1447          })
1448          .each((r) => {
1449            ListItem() {
1450              Text(r.index! + ":" + r.item + "eachMessage");
1451            }
1452          })
1453      }
1454      .onScrollIndex((start, end) => {
1455        this.start = start;
1456        this.end = end;
1457      })
1458      .height('30%')
1459
1460      Button(`insert totalCount ${this.totalCount}`)
1461        .height(60)
1462        .onClick(() => {
1463          // Insert an element which locates in the previous position displayed on the screen.
1464          this.arrayHolder.arr.splice(18, 0, this.totalCount);
1465          let rect = this.scroller.getItemRect(this.start); // Obtain the size and position of the child component.
1466          this.scroller.scrollToIndex(this.start + 1); // Slide to the specified index.
1467          this.scroller.scrollBy(0, -rect.y); // Slide by a specified distance.
1468          this.totalCount = this.arrayHolder.arr.length;
1469        })
1470    }
1471    .width('100%')
1472    .margin({ top: 5 })
1473  }
1474}
1475```
1476
1477The figure below shows the effect.
1478
1479![Repeat-case1-Succ](./figures/Repeat-Case1-Succ.gif)
1480
1481### The totalCount Value Is Greater Than the Length of Data Source
1482
1483When 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**.
1484
1485If **totalCount** is greater than **array.length**, the application should request subsequent data when the list scrolls 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.
1486
1487You 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:
1488
1489```ts
1490@ObservedV2
1491class VehicleData {
1492  @Trace name: string;
1493  @Trace price: number;
1494
1495  constructor(name: string, price: number) {
1496    this.name = name;
1497    this.price = price;
1498  }
1499}
1500
1501@ObservedV2
1502class VehicleDB {
1503  public vehicleItems: VehicleData[] = [];
1504
1505  constructor() {
1506    // The initial size of the array is 20.
1507    for (let i = 1; i <= 20; i++) {
1508      this.vehicleItems.push(new VehicleData(`Vehicle${i}`, i));
1509    }
1510  }
1511}
1512
1513@Entry
1514@ComponentV2
1515struct entryCompSucc {
1516  @Local vehicleItems: VehicleData[] = new VehicleDB().vehicleItems;
1517  @Local listChildrenSize: ChildrenMainSize = new ChildrenMainSize(60);
1518  @Local totalCount: number = this.vehicleItems.length;
1519  scroller: Scroller = new Scroller();
1520
1521  build() {
1522    Column({ space: 3 }) {
1523      List({ scroller: this.scroller }) {
1524        Repeat(this.vehicleItems)
1525          .virtualScroll({ totalCount: 50 }) // The expected array length is 50.
1526          .templateId(() => 'default')
1527          .template('default', (ri) => {
1528            ListItem() {
1529              Column() {
1530                Text(`${ri.item.name} + ${ri.index}`)
1531                  .width('90%')
1532                  .height(this.listChildrenSize.childDefaultSize)
1533                  .backgroundColor(0xFFA07A)
1534                  .textAlign(TextAlign.Center)
1535                  .fontSize(20)
1536                  .fontWeight(FontWeight.Bold)
1537              }
1538            }.border({ width: 1 })
1539          }, { cachedCount: 5 })
1540          .each((ri) => {
1541            ListItem() {
1542              Text("Wrong: " + `${ri.item.name} + ${ri.index}`)
1543                .width('90%')
1544                .height(this.listChildrenSize.childDefaultSize)
1545                .backgroundColor(0xFFA07A)
1546                .textAlign(TextAlign.Center)
1547                .fontSize(20)
1548                .fontWeight(FontWeight.Bold)
1549            }.border({ width: 1 })
1550          })
1551          .key((item, index) => `${index}:${item}`)
1552      }
1553      .height('50%')
1554      .margin({ top: 20 })
1555      .childrenMainSize(this.listChildrenSize)
1556      .alignListItem(ListItemAlign.Center)
1557      .onScrollIndex((start, end) => {
1558        console.log('onScrollIndex', start, end);
1559        // Lazy loading
1560        if (this.vehicleItems.length < 50) {
1561          for (let i = 0; i < 10; i++) {
1562            if (this.vehicleItems.length < 50) {
1563              this.vehicleItems.push(new VehicleData("Vehicle_loaded", i));
1564            }
1565          }
1566        }
1567      })
1568    }
1569  }
1570}
1571```
1572
1573The figure below shows the effect.
1574
1575![Repeat-Case2-Succ](./figures/Repeat-Case2-Succ.gif)
1576
1577### Constraints on the Mixed Use of Repeat and @Builder
1578
1579When **Repeat** and @Builder are used together, 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.
1580
1581The sample code is as follows:
1582
1583```ts
1584@Entry
1585@ComponentV2
1586struct RepeatBuilderPage {
1587  @Local simpleList1: Array<number> = [];
1588  @Local simpleList2: Array<number> = [];
1589
1590  aboutToAppear(): void {
1591    for (let i = 0; i < 100; i++) {
1592      this.simpleList1.push(i)
1593      this.simpleList2.push(i)
1594    }
1595  }
1596
1597  build() {
1598    Column({ space: 20 }) {
1599      Text('Use Repeat and @Builder together: The abnormal display is on the left, and the normal display is on the right.')
1600        .fontSize(15)
1601        .fontColor(Color.Gray)
1602
1603      Row({ space: 20 }) {
1604        List({ initialIndex: 5, space: 20 }) {
1605          Repeat<number>(this.simpleList1)
1606            .each((ri) => {})
1607            .virtualScroll({ totalCount: this.simpleList1.length })
1608            .templateId((item: number, index: number) => "default")
1609            .template('default', (ri) => {
1610              ListItem() {
1611                Column() {
1612                  Text('Text id = ' + ri.item)
1613                    .fontSize(20)
1614                  this.buildItem1 (ri.item) // Change to this.buildItem1(ri).
1615                }
1616              }
1617              .border({ width: 1 })
1618            }, { cachedCount: 3 })
1619        }
1620        .cachedCount(1)
1621        .border({ width: 1 })
1622        .width('45%')
1623        .height('60%')
1624
1625        List({ initialIndex: 5, space: 20 }) {
1626          Repeat<number>(this.simpleList2)
1627            .each((ri) => {})
1628            .virtualScroll({ totalCount: this.simpleList2.length })
1629            .templateId((item: number, index: number) => "default")
1630            .template('default', (ri) => {
1631              ListItem() {
1632                Column() {
1633                  Text('Text id = ' + ri.item)
1634                    .fontSize(20)
1635                  this.buildItem2(ri)
1636                }
1637              }
1638              .border({ width: 1 })
1639            }, { cachedCount: 3 })
1640        }
1641        .cachedCount(1)
1642        .border({ width: 1 })
1643        .width('45%')
1644        .height('60%')
1645      }
1646    }
1647    .height('100%')
1648    .justifyContent(FlexAlign.Center)
1649  }
1650
1651  @Builder
1652  // The @Builder parameter must be of the RepeatItem type for normal rendering.
1653  buildItem1(item: number) {
1654    Text('Builder1 id = ' + item)
1655      .fontSize(20)
1656      .fontColor(Color.Red)
1657      .margin({ top: 2 })
1658  }
1659
1660  @Builder
1661  buildItem2(ri: RepeatItem<number>) {
1662    Text('Builder2 id = ' + ri.item)
1663      .fontSize(20)
1664      .fontColor(Color.Red)
1665      .margin({ top: 2 })
1666  }
1667}
1668```
1669
1670The 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.
1671
1672![Repeat-Builder](./figures/Repeat-Builder.png)
1673