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 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 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 130 131The following figure shows an example of array changes. 132 133 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 1673