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