1# \@Reusable Decorator: Reusing Components 2<!--Kit: ArkUI--> 3<!--Subsystem: ArkUI--> 4<!--Owner: @liyujie43--> 5<!--Designer: @lizhan--> 6<!--Tester: @TerryTsao--> 7<!--Adviser: @zhang_yixin13--> 8 9The \@Reusable decorator enables reuse of view nodes, component instances, and state contexts for custom components, eliminating redundant creation and destruction to enhance performance. 10 11## Overview 12 13When applied to a custom component, the \@Reusable decorator marks the component as reusable. Used in conjunction with the [\@Component decorator](arkts-create-custom-components.md#component), components decorated with \@Reusable are moved to a reuse cache (along with its corresponding JS object) when removed from the component tree. Subsequent component creation will reuse cached nodes, significantly reducing instantiation time. 14 15> **NOTE** 16> 17> The \@Reusable decorator is supported since API version 10 and can be used in ArkTS. 18> 19> For details about the principles, optimization methods, and use scenarios of component reuse, see [Component Reuse](https://developer.huawei.com/consumer/en/doc/best-practices/bpta-component-reuse). 20> 21> When a component is decorated with \@Reusable, the ArkUI framework calls the component's [aboutToReuse](../../../application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttoreuse10) and [aboutToRecycle](../../../application-dev/reference/apis-arkui/arkui-ts/ts-custom-component-lifecycle.md#abouttorecycle10) APIs when the component is added to or removed from the tree. Therefore, you should implement most reuse logic within these APIs. 22> 23> For components containing multiple reusable child components, use [reuseId](../../../application-dev/reference/apis-arkui/arkui-ts/ts-universal-attributes-reuse-id.md) to distinguish between different reusable structures. 24> 25 26## Constraints 27 28- The \@Reusable decorator only applies to custom components. 29 30```ts 31import { ComponentContent } from "@kit.ArkUI"; 32 33// Adding @Reusable to @Builder causes a compilation error (not applicable to builders). 34// @Reusable 35@Builder 36function buildCreativeLoadingDialog(closedClick: () => void) { 37 Crash() 38} 39 40@Component 41export struct Crash { 42 build() { 43 Column() { 44 Text("Crash") 45 .fontSize(12) 46 .lineHeight(18) 47 .fontColor(Color.Blue) 48 .margin({ 49 left: 6 50 }) 51 }.width('100%') 52 .height('100%') 53 .justifyContent(FlexAlign.Center) 54 } 55} 56 57@Entry 58@Component 59struct Index { 60 @State message: string = 'Hello World'; 61 private uiContext = this.getUIContext(); 62 63 build() { 64 RelativeContainer() { 65 Text(this.message) 66 .id('Index') 67 .fontSize(50) 68 .fontWeight(FontWeight.Bold) 69 .alignRules({ 70 center: { anchor: '__container__', align: VerticalAlign.Center }, 71 middle: { anchor: '__container__', align: HorizontalAlign.Center } 72 }) 73 .onClick(() => { 74 let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => { 75 }); 76 this.uiContext.getPromptAction().openCustomDialog(contentNode); 77 }) 78 } 79 .height('100%') 80 .width('100%') 81 } 82} 83``` 84 85- When an @Reusable decorated custom component is reused, the **aboutToReuse** API is invoked recursively for the component and all its child components. Avoid modifying state variables of the parent component in its child component's **aboutToReuse** API. Such modification will not take effect. To update the parent component's state variables, use **setTimeout** to delay execution, moving the task outside the scope of component reuse. 86 87 88 **Incorrect Usage** 89 90 Modifying a parent component's state variable in its child component's **aboutToReuse** API: 91 92 ```ts 93 class BasicDataSource implements IDataSource { 94 private listener: DataChangeListener | undefined = undefined; 95 public dataArray: number[] = []; 96 97 totalCount(): number { 98 return this.dataArray.length; 99 } 100 101 getData(index: number): number { 102 return this.dataArray[index]; 103 } 104 105 registerDataChangeListener(listener: DataChangeListener): void { 106 this.listener = listener; 107 } 108 109 unregisterDataChangeListener(listener: DataChangeListener): void { 110 this.listener = undefined; 111 } 112 } 113 114 @Entry 115 @Component 116 struct Index { 117 private data: BasicDataSource = new BasicDataSource(); 118 119 aboutToAppear(): void { 120 for (let index = 1; index < 20; index++) { 121 this.data.dataArray.push(index); 122 } 123 } 124 125 build() { 126 List() { 127 LazyForEach(this.data, (item: number, index: number) => { 128 ListItem() { 129 ReuseComponent({ num: item }) 130 } 131 }, (item: number, index: number) => index.toString()) 132 }.cachedCount(0) 133 } 134 } 135 136 @Reusable 137 @Component 138 struct ReuseComponent { 139 @State num: number = 0; 140 141 aboutToReuse(params: ESObject): void { 142 this.num = params.num; 143 } 144 145 build() { 146 Column() { 147 Text('ReuseComponent num:' + this.num.toString()) 148 ReuseComponentChild({ num: this.num }) 149 Button('plus') 150 .onClick(() => { 151 this.num += 10; 152 }) 153 } 154 .height(200) 155 } 156 } 157 158 @Component 159 struct ReuseComponentChild { 160 @Link num: number; 161 162 aboutToReuse(params: ESObject): void { 163 this.num = -1 * params.num; 164 } 165 166 build() { 167 Text('ReuseComponentChild num:' + this.num.toString()) 168 } 169 } 170 ``` 171 172 **Correct Usage** 173 174 To modify a parent component's state variable in a child component's **aboutToReuse** API, use **setTimeout** to move the modification outside the scope of component reuse: 175 176 ```ts 177 class BasicDataSource implements IDataSource { 178 private listener: DataChangeListener | undefined = undefined; 179 public dataArray: number[] = []; 180 181 totalCount(): number { 182 return this.dataArray.length; 183 } 184 185 getData(index: number): number { 186 return this.dataArray[index]; 187 } 188 189 registerDataChangeListener(listener: DataChangeListener): void { 190 this.listener = listener; 191 } 192 193 unregisterDataChangeListener(listener: DataChangeListener): void { 194 this.listener = undefined; 195 } 196 } 197 198 @Entry 199 @Component 200 struct Index { 201 private data: BasicDataSource = new BasicDataSource(); 202 203 aboutToAppear(): void { 204 for (let index = 1; index < 20; index++) { 205 this.data.dataArray.push(index); 206 } 207 } 208 209 build() { 210 List() { 211 LazyForEach(this.data, (item: number, index: number) => { 212 ListItem() { 213 ReuseComponent({ num: item }) 214 } 215 }, (item: number, index: number) => index.toString()) 216 }.cachedCount(0) 217 } 218 } 219 220 @Reusable 221 @Component 222 struct ReuseComponent { 223 @State num: number = 0; 224 225 aboutToReuse(params: ESObject): void { 226 this.num = params.num; 227 } 228 229 build() { 230 Column() { 231 Text('ReuseComponent num:' + this.num.toString()) 232 ReuseComponentChild({ num: this.num }) 233 Button('plus') 234 .onClick(() => { 235 this.num += 10; 236 }) 237 } 238 .height(200) 239 } 240 } 241 242 @Component 243 struct ReuseComponentChild { 244 @Link num: number; 245 246 aboutToReuse(params: ESObject): void { 247 setTimeout(() => { 248 this.num = -1 * params.num; 249 }, 1) 250 } 251 252 build() { 253 Text('ReuseComponentChild num:' + this.num.toString()) 254 } 255 } 256 ``` 257 258- **ComponentContent** does not support passing \@Reusable decorated custom components. 259 260```ts 261import { ComponentContent } from "@kit.ArkUI"; 262 263@Builder 264function buildCreativeLoadingDialog(closedClick: () => void) { 265 Crash() 266} 267 268// The dialog box pops up correctly if @Reusable is commented out; it crashes when @Reusable is added. 269@Reusable 270@Component 271export struct Crash { 272 build() { 273 Column() { 274 Text("Crash") 275 .fontSize(12) 276 .lineHeight(18) 277 .fontColor(Color.Blue) 278 .margin({ 279 left: 6 280 }) 281 }.width('100%') 282 .height('100%') 283 .justifyContent(FlexAlign.Center) 284 } 285} 286 287@Entry 288@Component 289struct Index { 290 @State message: string = 'Hello World'; 291 private uiContext = this.getUIContext(); 292 293 build() { 294 RelativeContainer() { 295 Text(this.message) 296 .id('Index') 297 .fontSize(50) 298 .fontWeight(FontWeight.Bold) 299 .alignRules({ 300 center: { anchor: '__container__', align: VerticalAlign.Center }, 301 middle: { anchor: '__container__', align: HorizontalAlign.Center } 302 }) 303 .onClick(() => { 304 // ComponentContent is based on BuilderNode, which does not support @Reusable decorated custom components. 305 let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => { 306 }); 307 this.uiContext.getPromptAction().openCustomDialog(contentNode); 308 }) 309 } 310 .height('100%') 311 .width('100%') 312 } 313} 314``` 315 316- Nesting \@Reusable decorators is not recommended, as it increases memory usage, reduces reuse efficiency, and complicates maintenance. Nested usage creates additional cache pools with identical tree structures, leading to low reuse efficiency. In addition, it complicates lifecycle management and makes resource and variable sharing difficult. 317 318 319## Use Scenarios 320 321### Dynamic Layout Update 322 323Repeatedly creating and removing views can trigger frequent layout calculations, which may affect frame rates. Component reuse avoids unnecessary view creation and layout recalculations, improving performance. 324In the following example, the **Child** custom component is marked as reusable. Clicking the button updates **Child**, triggering reuse. 325 326```ts 327// xxx.ets 328export class Message { 329 value: string | undefined; 330 331 constructor(value: string) { 332 this.value = value; 333 } 334} 335 336@Entry 337@Component 338struct Index { 339 @State switch: boolean = true; 340 341 build() { 342 Column() { 343 Button('Hello') 344 .fontSize(30) 345 .fontWeight(FontWeight.Bold) 346 .onClick(() => { 347 this.switch = !this.switch; 348 }) 349 if (this.switch) { 350 // If only one reusable component is used, reuseId is optional. 351 Child({ message: new Message('Child') }) 352 .reuseId('Child') 353 } 354 } 355 .height("100%") 356 .width('100%') 357 } 358} 359 360@Reusable 361@Component 362struct Child { 363 @State message: Message = new Message('AboutToReuse'); 364 365 aboutToReuse(params: Record<string, ESObject>) { 366 console.info("Recycle====Child=="); 367 this.message = params.message as Message; 368 } 369 370 build() { 371 Column() { 372 Text(this.message.value) 373 .fontSize(30) 374 } 375 .borderWidth(1) 376 .height(100) 377 } 378} 379``` 380 381### List Scrolling with LazyForEach 382 383- When a user scrolls a list containing a large amount of data, frequent creation and destruction of list items can cause lag and performance issues. The reuse mechanism of the **List** component can reuse the existing list items to improve the scrolling smoothness. 384 385- In the following example, the **CardView** custom component is marked as reusable. Scrolling the list up or down triggers reuse of **CardView**. 386 387```ts 388class MyDataSource implements IDataSource { 389 private dataArray: string[] = []; 390 private listener: DataChangeListener | undefined; 391 392 public totalCount(): number { 393 return this.dataArray.length; 394 } 395 396 public getData(index: number): string { 397 return this.dataArray[index]; 398 } 399 400 public pushData(data: string): void { 401 this.dataArray.push(data); 402 } 403 404 public reloadListener(): void { 405 this.listener?.onDataReloaded(); 406 } 407 408 public registerDataChangeListener(listener: DataChangeListener): void { 409 this.listener = listener; 410 } 411 412 public unregisterDataChangeListener(listener: DataChangeListener): void { 413 this.listener = undefined; 414 } 415} 416 417@Entry 418@Component 419struct ReuseDemo { 420 private data: MyDataSource = new MyDataSource(); 421 422 aboutToAppear() { 423 for (let i = 1; i < 1000; i++) { 424 this.data.pushData(i + ""); 425 } 426 } 427 428 // ... 429 build() { 430 Column() { 431 List() { 432 LazyForEach(this.data, (item: string) => { 433 ListItem() { 434 CardView({ item: item }) 435 } 436 }, (item: string) => item) 437 } 438 } 439 } 440} 441 442// Reusable component 443@Reusable 444@Component 445export struct CardView { 446 // Variables decorated with @State will update; others will not. 447 @State item: string = ''; 448 449 aboutToReuse(params: Record<string, Object>): void { 450 this.item = params.item as string; 451 } 452 453 build() { 454 Column() { 455 Text(this.item) 456 .fontSize(30) 457 } 458 .borderWidth(1) 459 .height(100) 460 } 461} 462``` 463 464### List Scrolling with if Statements 465 466In the following example, the **OneMoment** custom component is marked as reusable. Scrolling the list up or down triggers reuse of **OneMoment**. **reuseId** can be used to assign a reuse group for reusable components. Components with the same **reuseId** are reused within the same reuse group. A single reusable component does not require **reuseId**. Using **reuseId** to identify reusable components avoids repeated deletion and re-creation logic in **if** statements, improving reuse efficiency and performance. 467 468```ts 469@Entry 470@Component 471struct Index { 472 private dataSource = new MyDataSource<FriendMoment>(); 473 474 aboutToAppear(): void { 475 for (let i = 0; i < 20; i++) { 476 let title = i + 1 + "test_if"; 477 this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon')); 478 } 479 480 for (let i = 0; i < 50; i++) { 481 let title = i + 1 + "test_if"; 482 this.dataSource.pushData(new FriendMoment(i.toString(), title, '')); 483 } 484 } 485 486 build() { 487 Column() { 488 // TopBar() 489 List({ space: 3 }) { 490 LazyForEach(this.dataSource, (moment: FriendMoment) => { 491 ListItem() { 492 // Use reuseId to control component reuse. 493 OneMoment({ moment: moment }) 494 .reuseId((moment.image !== '') ? 'withImage' : 'noImage') 495 } 496 }, (moment: FriendMoment) => moment.id) 497 } 498 .cachedCount(0) 499 } 500 } 501} 502 503class FriendMoment { 504 id: string = ''; 505 text: string = ''; 506 title: string = ''; 507 image: string = ''; 508 answers: Array<ResourceStr> = []; 509 510 constructor(id: string, title: string, image: string) { 511 this.text = id; 512 this.title = title; 513 this.image = image; 514 } 515} 516 517@Reusable 518@Component 519export struct OneMoment { 520 @Prop moment: FriendMoment; 521 522 // Only components with the same reuseId trigger reuse. 523 aboutToReuse(params: ESObject): void { 524 console.log("=====aboutToReuse====OneMoment==reused==" + this.moment.text); 525 } 526 527 build() { 528 Column() { 529 Text(this.moment.text) 530 // Conditional rendering with if 531 if (this.moment.image !== '') { 532 Flex({ wrap: FlexWrap.Wrap }) { 533 Image($r(this.moment.image)).height(50).width(50) 534 Image($r(this.moment.image)).height(50).width(50) 535 Image($r(this.moment.image)).height(50).width(50) 536 Image($r(this.moment.image)).height(50).width(50) 537 } 538 } 539 } 540 } 541} 542 543class BasicDataSource<T> implements IDataSource { 544 private listeners: DataChangeListener[] = []; 545 private originDataArray: T[] = []; 546 547 public totalCount(): number { 548 return 0; 549 } 550 551 public getData(index: number): T { 552 return this.originDataArray[index]; 553 } 554 555 registerDataChangeListener(listener: DataChangeListener): void { 556 if (this.listeners.indexOf(listener) < 0) { 557 this.listeners.push(listener); 558 } 559 } 560 561 unregisterDataChangeListener(listener: DataChangeListener): void { 562 const pos = this.listeners.indexOf(listener); 563 if (pos >= 0) { 564 this.listeners.splice(pos, 1); 565 } 566 } 567 568 notifyDataAdd(index: number): void { 569 this.listeners.forEach(listener => { 570 listener.onDataAdd(index); 571 }); 572 } 573} 574 575export class MyDataSource<T> extends BasicDataSource<T> { 576 private dataArray: T[] = []; 577 578 public totalCount(): number { 579 return this.dataArray.length; 580 } 581 582 public getData(index: number): T { 583 return this.dataArray[index]; 584 } 585 586 public pushData(data: T): void { 587 this.dataArray.push(data); 588 this.notifyDataAdd(this.dataArray.length - 1); 589 } 590} 591``` 592 593### List Scrolling with ForEach 594 595When the **ForEach** rendering control syntax is used to create reusable custom components, the full-expansion behavior of **ForEach** prevents component reuse. In the example: Clicking **update** successfully refreshes data, but **ListItemView** cannot be reused during list scrolling; clicking **clear** and then **update** allows **ListItemView** to be reused, as this triggers re-creation of multiple destroyed custom components within a single frame. 596 597```ts 598// xxx.ets 599class MyDataSource implements IDataSource { 600 private dataArray: string[] = []; 601 602 public totalCount(): number { 603 return this.dataArray.length; 604 } 605 606 public getData(index: number): string { 607 return this.dataArray[index]; 608 } 609 610 public pushData(data: string): void { 611 this.dataArray.push(data); 612 } 613 614 public registerDataChangeListener(listener: DataChangeListener): void { 615 } 616 617 public unregisterDataChangeListener(listener: DataChangeListener): void { 618 } 619} 620 621@Entry 622@Component 623struct Index { 624 private data: MyDataSource = new MyDataSource(); 625 private data02: MyDataSource = new MyDataSource(); 626 @State isShow: boolean = true; 627 @State dataSource: ListItemObject[] = []; 628 629 aboutToAppear() { 630 for (let i = 0; i < 100; i++) { 631 this.data.pushData(i.toString()); 632 } 633 634 for (let i = 30; i < 80; i++) { 635 this.data02.pushData(i.toString()); 636 } 637 } 638 639 build() { 640 Column() { 641 Row() { 642 Button('clear').onClick(() => { 643 for (let i = 1; i < 50; i++) { 644 this.dataSource.pop(); 645 } 646 }).height(40) 647 648 Button('update').onClick(() => { 649 for (let i = 1; i < 50; i++) { 650 let obj = new ListItemObject(); 651 obj.id = i; 652 obj.uuid = Math.random().toString(); 653 obj.isExpand = false; 654 this.dataSource.push(obj); 655 } 656 }).height(40) 657 } 658 659 List({ space: 10 }) { 660 ForEach(this.dataSource, (item: ListItemObject) => { 661 ListItem() { 662 ListItemView({ 663 obj: item 664 }) 665 } 666 }, (item: ListItemObject) => { 667 return item.uuid.toString(); 668 }) 669 670 }.cachedCount(0) 671 .width('100%') 672 .height('100%') 673 } 674 } 675} 676 677@Reusable 678@Component 679struct ListItemView { 680 @ObjectLink obj: ListItemObject; 681 @State item: string = ''; 682 683 aboutToAppear(): void { 684 // On first update click, scrolling fails to trigger reuse due to the full-expansion behavior of ForEach. 685 console.log("=====aboutToAppear=====ListItemView==created==" + this.item); 686 } 687 688 aboutToReuse(params: ESObject) { 689 this.item = params.item; 690 // Reuse succeeds after clear and update are clicked 691 // (which recreates destroyed components in one frame). 692 console.log("=====aboutToReuse====ListItemView==reused==" + this.item); 693 } 694 695 build() { 696 Column({ space: 10 }) { 697 Text(`${this.obj.id}.Title`) 698 .fontSize(16) 699 .fontColor('#000000') 700 .padding({ 701 top: 20, 702 bottom: 20, 703 }) 704 705 if (this.obj.isExpand) { 706 Text('') 707 .fontSize(14) 708 .fontColor('#999999') 709 } 710 } 711 .width('100%') 712 .borderRadius(10) 713 .backgroundColor(Color.White) 714 .padding(15) 715 .onClick(() => { 716 this.obj.isExpand = !this.obj.isExpand; 717 }) 718 } 719} 720 721@Observed 722class ListItemObject { 723 uuid: string = ""; 724 id: number = 0; 725 isExpand: boolean = false; 726} 727``` 728 729### Grid 730 731In the following example, the @Reusable decorator is used to decorate the custom component **ReusableChildComponent** in **GridItem**, indicating that the component can be reused. 732The **aboutToReuse** API is triggered when the component is obtained from the reuse cache and added to the component tree during grid scrolling. This allows you to update the component's state variables to display correct content. 733Note: There is no need to update state variables that automatically synchronize values (such as variables decorated with [\@Link](arkts-link.md), [\@StorageLink](arkts-appstorage.md#storagelink), [\@ObjectLink](arkts-observed-and-objectlink.md), or [\@Consume](arkts-provide-and-consume.md)) in **aboutToReuse**, as this may trigger unnecessary component re-renders. 734 735```ts 736// Class MyDataSource implements the IDataSource API. 737class MyDataSource implements IDataSource { 738 private dataArray: number[] = []; 739 740 public pushData(data: number): void { 741 this.dataArray.push(data); 742 } 743 744 // Total number of items in the data source. 745 public totalCount(): number { 746 return this.dataArray.length; 747 } 748 749 // Return data at the specified index. 750 public getData(index: number): number { 751 return this.dataArray[index]; 752 } 753 754 registerDataChangeListener(listener: DataChangeListener): void { 755 } 756 757 unregisterDataChangeListener(listener: DataChangeListener): void { 758 } 759} 760 761@Entry 762@Component 763struct MyComponent { 764 // Data source. 765 private data: MyDataSource = new MyDataSource(); 766 767 aboutToAppear() { 768 for (let i = 1; i < 1000; i++) { 769 this.data.pushData(i); 770 } 771 } 772 773 build() { 774 Column({ space: 5 }) { 775 Grid() { 776 LazyForEach(this.data, (item: number) => { 777 GridItem() { 778 // Use the reusable custom component. 779 ReusableChildComponent({ item: item }) 780 } 781 }, (item: string) => item) 782 } 783 .cachedCount(2) // Set the number of cached GridItem components. 784 .columnsTemplate('1fr 1fr 1fr') 785 .columnsGap(10) 786 .rowsGap(10) 787 .margin(10) 788 .height(500) 789 .backgroundColor(0xFAEEE0) 790 } 791 } 792} 793 794@Reusable 795@Component 796struct ReusableChildComponent { 797 @State item: number = 0; 798 799 // Called before the component is added to the component tree from the reuse cache. The component's state variable can be updated here to display the correct content. 800 // Parameter type: Record<string, number> (explicit type instead of any). 801 aboutToReuse(params: Record<string, number>) { 802 this.item = params.item; 803 } 804 805 build() { 806 Column() { 807 // Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error. 808 Image($r('app.media.app_icon')) 809 .objectFit(ImageFit.Fill) 810 .layoutWeight(1) 811 Text(`Image${this.item}`) 812 .fontSize(16) 813 .textAlign(TextAlign.Center) 814 } 815 .width('100%') 816 .height(120) 817 .backgroundColor(0xF9CF93) 818 } 819} 820``` 821 822### WaterFlow 823 824- For **WaterFlow** scrolling scenarios where **FlowItem** and its child components are frequently created and destroyed, you can encapsulate components in **FlowItem** into a custom component and decorate it with \@Reusable to implement component reuse. 825 826```ts 827class WaterFlowDataSource implements IDataSource { 828 private dataArray: number[] = []; 829 private listeners: DataChangeListener[] = []; 830 831 constructor() { 832 for (let i = 0; i <= 60; i++) { 833 this.dataArray.push(i); 834 } 835 } 836 837 // Obtain data at the specified index. 838 public getData(index: number): number { 839 return this.dataArray[index]; 840 } 841 842 // Notify listeners of new data addition. 843 notifyDataAdd(index: number): void { 844 this.listeners.forEach(listener => { 845 listener.onDataAdd(index); 846 }); 847 } 848 849 // Obtain the total number of data items. 850 public totalCount(): number { 851 return this.dataArray.length; 852 } 853 854 // Register a data change listener. 855 registerDataChangeListener(listener: DataChangeListener): void { 856 if (this.listeners.indexOf(listener) < 0) { 857 this.listeners.push(listener); 858 } 859 } 860 861 // Unregister the data change listener. 862 unregisterDataChangeListener(listener: DataChangeListener): void { 863 const pos = this.listeners.indexOf(listener); 864 if (pos >= 0) { 865 this.listeners.splice(pos, 1); 866 } 867 } 868 869 // Add an item to the end of the data array. 870 public addLastItem(): void { 871 this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length); 872 this.notifyDataAdd(this.dataArray.length - 1); 873 } 874} 875 876@Reusable 877@Component 878struct ReusableFlowItem { 879 @State item: number = 0; 880 881 // Called before the component is added to the component tree from the reuse cache. The component's state variable can be updated here to display the correct content. 882 aboutToReuse(params: ESObject) { 883 this.item = params.item; 884 console.log("=====aboutToReuse====FlowItem==reused==" + this.item); 885 } 886 887 aboutToRecycle(): void { 888 console.log("=====aboutToRecycle====FlowItem==recycled==" + this.item); 889 } 890 891 build() { 892 // Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error. 893 Column() { 894 Text("N" + this.item).fontSize(24).height('26').margin(10) 895 Image($r('app.media.app_icon')) 896 .objectFit(ImageFit.Cover) 897 .width(50) 898 .height(50) 899 } 900 } 901} 902 903@Entry 904@Component 905struct Index { 906 @State minSize: number = 50; 907 @State maxSize: number = 80; 908 @State fontSize: number = 24; 909 @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F]; 910 scroller: Scroller = new Scroller(); 911 dataSource: WaterFlowDataSource = new WaterFlowDataSource(); 912 private itemWidthArray: number[] = []; 913 private itemHeightArray: number[] = []; 914 915 // Calculate random size for flow items. 916 getSize() { 917 let ret = Math.floor(Math.random() * this.maxSize); 918 return (ret > this.minSize ? ret : this.minSize); 919 } 920 921 // Generate size arrays for flow items. 922 getItemSizeArray() { 923 for (let i = 0; i < 100; i++) { 924 this.itemWidthArray.push(this.getSize()); 925 this.itemHeightArray.push(this.getSize()); 926 } 927 } 928 929 aboutToAppear() { 930 this.getItemSizeArray(); 931 } 932 933 build() { 934 Stack({ alignContent: Alignment.TopStart }) { 935 Column({ space: 2 }) { 936 Button('back top') 937 .height('5%') 938 .onClick(() => { 939 940 // Scroll to top when the component is clicked. 941 this.scroller.scrollEdge(Edge.Top); 942 }) 943 WaterFlow({ scroller: this.scroller }) { 944 LazyForEach(this.dataSource, (item: number) => { 945 FlowItem() { 946 ReusableFlowItem({ item: item }) 947 }.onAppear(() => { 948 if (item + 20 == this.dataSource.totalCount()) { 949 for (let i = 0; i < 50; i++) { 950 this.dataSource.addLastItem(); 951 } 952 } 953 }) 954 955 }) 956 } 957 } 958 } 959 } 960} 961``` 962 963### Swiper 964 965- For **Swiper** scrolling scenarios where child components are frequently created and destroyed, you can encapsulate the child components into a custom component and decorate it with \@Reusable to implement component reuse. 966 967```ts 968@Entry 969@Component 970struct Index { 971 private dataSource = new MyDataSource<Question>(); 972 973 aboutToAppear(): void { 974 for (let i = 0; i < 1000; i++) { 975 let title = i + 1 + "test_swiper"; 976 let answers = ["test1", "test2", "test3", 977 "test4"]; 978 // Ensure that the app.media.app_icon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error. 979 this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers)); 980 } 981 } 982 983 build() { 984 Column({ space: 5 }) { 985 Swiper() { 986 LazyForEach(this.dataSource, (item: Question) => { 987 QuestionSwiperItem({ itemData: item }) 988 }, (item: Question) => item.id) 989 } 990 } 991 .width('100%') 992 .margin({ top: 5 }) 993 } 994} 995 996class Question { 997 id: string = ''; 998 title: ResourceStr = ''; 999 image: ResourceStr = ''; 1000 answers: Array<ResourceStr> = []; 1001 1002 constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) { 1003 this.id = id; 1004 this.title = title; 1005 this.image = image; 1006 this.answers = answers; 1007 } 1008} 1009 1010@Reusable 1011@Component 1012struct QuestionSwiperItem { 1013 @State itemData: Question | null = null; 1014 1015 aboutToReuse(params: Record<string, Object>): void { 1016 this.itemData = params.itemData as Question; 1017 console.info("===aboutToReuse====QuestionSwiperItem=="); 1018 } 1019 1020 build() { 1021 Column() { 1022 Text(this.itemData?.title) 1023 .fontSize(18) 1024 .fontColor($r('sys.color.ohos_id_color_primary')) 1025 .alignSelf(ItemAlign.Start) 1026 .margin({ 1027 top: 10, 1028 bottom: 16 1029 }) 1030 Image(this.itemData?.image) 1031 .width('100%') 1032 .borderRadius(12) 1033 .objectFit(ImageFit.Contain) 1034 .margin({ 1035 bottom: 16 1036 }) 1037 .height(80) 1038 .width(80) 1039 1040 Column({ space: 16 }) { 1041 ForEach(this.itemData?.answers, (item: Resource) => { 1042 Text(item) 1043 .fontSize(16) 1044 .fontColor($r('sys.color.ohos_id_color_primary')) 1045 }, (item: ResourceStr) => JSON.stringify(item)) 1046 } 1047 .width('100%') 1048 .alignItems(HorizontalAlign.Start) 1049 } 1050 .width('100%') 1051 .padding({ 1052 left: 16, 1053 right: 16 1054 }) 1055 } 1056} 1057 1058class BasicDataSource<T> implements IDataSource { 1059 private listeners: DataChangeListener[] = []; 1060 private originDataArray: T[] = []; 1061 1062 public totalCount(): number { 1063 return 0; 1064 } 1065 1066 public getData(index: number): T { 1067 return this.originDataArray[index]; 1068 } 1069 1070 registerDataChangeListener(listener: DataChangeListener): void { 1071 if (this.listeners.indexOf(listener) < 0) { 1072 this.listeners.push(listener); 1073 } 1074 } 1075 1076 unregisterDataChangeListener(listener: DataChangeListener): void { 1077 const pos = this.listeners.indexOf(listener); 1078 if (pos >= 0) { 1079 this.listeners.splice(pos, 1); 1080 } 1081 } 1082 1083 notifyDataAdd(index: number): void { 1084 this.listeners.forEach(listener => { 1085 listener.onDataAdd(index); 1086 }); 1087 } 1088} 1089 1090export class MyDataSource<T> extends BasicDataSource<T> { 1091 private dataArray: T[] = []; 1092 1093 public totalCount(): number { 1094 return this.dataArray.length; 1095 } 1096 1097 public getData(index: number): T { 1098 return this.dataArray[index]; 1099 } 1100 1101 public pushData(data: T): void { 1102 this.dataArray.push(data); 1103 this.notifyDataAdd(this.dataArray.length - 1); 1104 } 1105} 1106``` 1107 1108### List Scrolling with ListItemGroup 1109 1110- For list scrolling scenarios where the **ListItemGroup** component is used, you can encapsulate child components in **ListItem** that need to be destroyed and re-created into a custom component and decorate it with \@Reusable to implement component reuse. 1111 1112```ts 1113@Entry 1114@Component 1115struct ListItemGroupAndReusable { 1116 data: DataSrc2 = new DataSrc2(); 1117 1118 @Builder 1119 itemHead(text: string) { 1120 Text(text) 1121 .fontSize(20) 1122 .backgroundColor(0xAABBCC) 1123 .width('100%') 1124 .padding(10) 1125 } 1126 1127 aboutToAppear() { 1128 for (let i = 0; i < 10000; i++) { 1129 let data_1 = new DataSrc1(); 1130 for (let j = 0; j < 12; j++) { 1131 data_1.Data.push(`Test item data: ${i} - ${j}`); 1132 } 1133 this.data.Data.push(data_1); 1134 } 1135 } 1136 1137 build() { 1138 Stack() { 1139 List() { 1140 LazyForEach(this.data, (item: DataSrc1, index: number) => { 1141 ListItemGroup({ header: this.itemHead(index.toString()) }) { 1142 LazyForEach(item, (ii: string, index: number) => { 1143 ListItem() { 1144 Inner({ str: ii }) 1145 } 1146 }) 1147 } 1148 .width('100%') 1149 .height('60vp') 1150 }) 1151 } 1152 } 1153 .width('100%') 1154 .height('100%') 1155 } 1156} 1157 1158@Reusable 1159@Component 1160struct Inner { 1161 @State str: string = ''; 1162 1163 aboutToReuse(param: ESObject) { 1164 this.str = param.str; 1165 } 1166 1167 build() { 1168 Text(this.str) 1169 } 1170} 1171 1172class DataSrc1 implements IDataSource { 1173 listeners: DataChangeListener[] = []; 1174 Data: string[] = []; 1175 1176 public totalCount(): number { 1177 return this.Data.length; 1178 } 1179 1180 public getData(index: number): string { 1181 return this.Data[index]; 1182 } 1183 1184 // Called by the framework to add a listener to the data source for LazyForEach. 1185 registerDataChangeListener(listener: DataChangeListener): void { 1186 if (this.listeners.indexOf(listener) < 0) { 1187 this.listeners.push(listener); 1188 } 1189 } 1190 1191 // Called by the framework to remove the listener from the data source for the corresponding LazyForEach component. 1192 unregisterDataChangeListener(listener: DataChangeListener): void { 1193 const pos = this.listeners.indexOf(listener); 1194 if (pos >= 0) { 1195 this.listeners.splice(pos, 1); 1196 } 1197 } 1198 1199 // Notify LazyForEach that all child components need to be reloaded. 1200 notifyDataReload(): void { 1201 this.listeners.forEach(listener => { 1202 listener.onDataReloaded(); 1203 }); 1204 } 1205 1206 // Notify LazyForEach that a child component needs to be added at the specified index. 1207 notifyDataAdd(index: number): void { 1208 this.listeners.forEach(listener => { 1209 listener.onDataAdd(index); 1210 }); 1211 } 1212 1213 // Notify LazyForEach that the data item at the specified index has changed and the child component needs to be rebuilt. 1214 notifyDataChange(index: number): void { 1215 this.listeners.forEach(listener => { 1216 listener.onDataChange(index); 1217 }); 1218 } 1219 1220 // Notify LazyForEach that the child component at the specified index needs to be deleted. 1221 notifyDataDelete(index: number): void { 1222 this.listeners.forEach(listener => { 1223 listener.onDataDelete(index); 1224 }); 1225 } 1226 1227 // Notify LazyForEach that data needs to be swapped between the from and to positions. 1228 notifyDataMove(from: number, to: number): void { 1229 this.listeners.forEach(listener => { 1230 listener.onDataMove(from, to); 1231 }); 1232 } 1233} 1234 1235class DataSrc2 implements IDataSource { 1236 listeners: DataChangeListener[] = []; 1237 Data: DataSrc1[] = []; 1238 1239 public totalCount(): number { 1240 return this.Data.length; 1241 } 1242 1243 public getData(index: number): DataSrc1 { 1244 return this.Data[index]; 1245 } 1246 1247 // Called by the framework to add a listener to the data source for LazyForEach. 1248 registerDataChangeListener(listener: DataChangeListener): void { 1249 if (this.listeners.indexOf(listener) < 0) { 1250 this.listeners.push(listener); 1251 } 1252 } 1253 1254 // Called by the framework to remove the listener from the data source for the corresponding LazyForEach component. 1255 unregisterDataChangeListener(listener: DataChangeListener): void { 1256 const pos = this.listeners.indexOf(listener); 1257 if (pos >= 0) { 1258 this.listeners.splice(pos, 1); 1259 } 1260 } 1261 1262 // Notify LazyForEach that all child components need to be reloaded. 1263 notifyDataReload(): void { 1264 this.listeners.forEach(listener => { 1265 listener.onDataReloaded(); 1266 }); 1267 } 1268 1269 // Notify LazyForEach that a child component needs to be added at the specified index. 1270 notifyDataAdd(index: number): void { 1271 this.listeners.forEach(listener => { 1272 listener.onDataAdd(index); 1273 }); 1274 } 1275 1276 // Notify LazyForEach that the data item at the specified index has changed and the child component needs to be rebuilt. 1277 notifyDataChange(index: number): void { 1278 this.listeners.forEach(listener => { 1279 listener.onDataChange(index); 1280 }); 1281 } 1282 1283 // Notify LazyForEach that the child component at the specified index needs to be deleted. 1284 notifyDataDelete(index: number): void { 1285 this.listeners.forEach(listener => { 1286 listener.onDataDelete(index); 1287 }); 1288 } 1289 1290 // Notify LazyForEach that data needs to be swapped between the from and to positions. 1291 notifyDataMove(from: number, to: number): void { 1292 this.listeners.forEach(listener => { 1293 listener.onDataMove(from, to); 1294 }); 1295 } 1296} 1297``` 1298 1299 1300### Scenarios Involving Multiple Item Types 1301 1302**Standard** 1303 1304Reusable components have the same layout. For implementation examples, see the description in the list scrolling sections. 1305 1306**Limited Variation** 1307 1308There are differences between reusable components, but the number of types is limited. For example, reuse can be achieved by explicitly setting two **reuseId** values or using two custom components. 1309 1310```ts 1311class MyDataSource implements IDataSource { 1312 private dataArray: string[] = []; 1313 private listener: DataChangeListener | undefined; 1314 1315 public totalCount(): number { 1316 return this.dataArray.length; 1317 } 1318 1319 public getData(index: number): string { 1320 return this.dataArray[index]; 1321 } 1322 1323 public pushData(data: string): void { 1324 this.dataArray.push(data); 1325 } 1326 1327 public reloadListener(): void { 1328 this.listener?.onDataReloaded(); 1329 } 1330 1331 public registerDataChangeListener(listener: DataChangeListener): void { 1332 this.listener = listener; 1333 } 1334 1335 public unregisterDataChangeListener(listener: DataChangeListener): void { 1336 this.listener = undefined; 1337 } 1338} 1339 1340@Entry 1341@Component 1342struct Index { 1343 private data: MyDataSource = new MyDataSource(); 1344 1345 aboutToAppear() { 1346 for (let i = 0; i < 1000; i++) { 1347 this.data.pushData(i + ""); 1348 } 1349 } 1350 1351 build() { 1352 Column() { 1353 List({ space: 10 }) { 1354 LazyForEach(this.data, (item: number) => { 1355 ListItem() { 1356 ReusableComponent({ item: item }) 1357 // Set two reuseId values with limited variations. 1358 .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo') 1359 } 1360 .backgroundColor(Color.Orange) 1361 .width('100%') 1362 }, (item: number) => item.toString()) 1363 } 1364 .cachedCount(2) 1365 } 1366 } 1367} 1368 1369@Reusable 1370@Component 1371struct ReusableComponent { 1372 @State item: number = 0; 1373 1374 aboutToReuse(params: ESObject) { 1375 this.item = params.item; 1376 } 1377 1378 build() { 1379 Column() { 1380 // Render according to type differences inside the component. 1381 if (this.item % 2 === 0) { 1382 Text(`Item ${this.item} ReusableComponentOne`) 1383 .fontSize(20) 1384 .margin({ left: 10 }) 1385 } else { 1386 Text(`Item ${this.item} ReusableComponentTwo`) 1387 .fontSize(20) 1388 .margin({ left: 10 }) 1389 } 1390 }.margin({ left: 10, right: 10 }) 1391 } 1392} 1393``` 1394 1395**Composite** 1396 1397There are multiple differences between reusable components, but they usually share common child components. In the example, after three reusable components are converted into **Builder** functions in a combined manner, the internal shared child components will be uniformly placed under the parent component **MyComponent**. The reuse cache is shared at the parent component level for child component reuse, reducing resource consumption during component creation. 1398 1399```ts 1400class MyDataSource implements IDataSource { 1401 private dataArray: string[] = []; 1402 private listener: DataChangeListener | undefined; 1403 1404 public totalCount(): number { 1405 return this.dataArray.length; 1406 } 1407 1408 public getData(index: number): string { 1409 return this.dataArray[index]; 1410 } 1411 1412 public pushData(data: string): void { 1413 this.dataArray.push(data); 1414 } 1415 1416 public reloadListener(): void { 1417 this.listener?.onDataReloaded(); 1418 } 1419 1420 public registerDataChangeListener(listener: DataChangeListener): void { 1421 this.listener = listener; 1422 } 1423 1424 public unregisterDataChangeListener(listener: DataChangeListener): void { 1425 this.listener = undefined; 1426 } 1427} 1428 1429@Entry 1430@Component 1431struct MyComponent { 1432 private data: MyDataSource = new MyDataSource(); 1433 1434 aboutToAppear() { 1435 for (let i = 0; i < 1000; i++) { 1436 this.data.pushData(i.toString()); 1437 } 1438 } 1439 1440 // The reusable component implementation of itemBuilderOne is not shown. Below is the Builder version after conversion. 1441 @Builder 1442 itemBuilderOne(item: string) { 1443 Column() { 1444 ChildComponentA({ item: item }) 1445 ChildComponentB({ item: item }) 1446 ChildComponentC({ item: item }) 1447 } 1448 } 1449 1450 // Builder version of itemBuilderTwo after conversion. 1451 @Builder 1452 itemBuilderTwo(item: string) { 1453 Column() { 1454 ChildComponentA({ item: item }) 1455 ChildComponentC({ item: item }) 1456 ChildComponentD({ item: item }) 1457 } 1458 } 1459 1460 // Builder version of itemBuilderThree after conversion. 1461 @Builder 1462 itemBuilderThree(item: string) { 1463 Column() { 1464 ChildComponentA({ item: item }) 1465 ChildComponentB({ item: item }) 1466 ChildComponentD({ item: item }) 1467 } 1468 } 1469 1470 build() { 1471 List({ space: 40 }) { 1472 LazyForEach(this.data, (item: string, index: number) => { 1473 ListItem() { 1474 if (index % 3 === 0) { 1475 this.itemBuilderOne(item) 1476 } else if (index % 5 === 0) { 1477 this.itemBuilderTwo(item) 1478 } else { 1479 this.itemBuilderThree(item) 1480 } 1481 } 1482 .backgroundColor('#cccccc') 1483 .width('100%') 1484 .onAppear(() => { 1485 console.log(`ListItem ${index} onAppear`); 1486 }) 1487 }, (item: number) => item.toString()) 1488 } 1489 .width('100%') 1490 .height('100%') 1491 .cachedCount(0) 1492 } 1493} 1494 1495@Reusable 1496@Component 1497struct ChildComponentA { 1498 @State item: string = ''; 1499 1500 aboutToReuse(params: ESObject) { 1501 console.log(`ChildComponentA ${params.item} Reuse ${this.item}`); 1502 this.item = params.item; 1503 } 1504 1505 aboutToRecycle(): void { 1506 console.log(`ChildComponentA ${this.item} Recycle`); 1507 } 1508 1509 build() { 1510 Column() { 1511 Text(`Item ${this.item} Child Component A`) 1512 .fontSize(20) 1513 .margin({ left: 10 }) 1514 .fontColor(Color.Blue) 1515 Grid() { 1516 ForEach((new Array(20)).fill(''), (item: string, index: number) => { 1517 GridItem() { 1518 // Ensure that the app.media.startIcon file is added to src/main/resources/base/media. Missing this file will trigger a runtime error. 1519 Image($r('app.media.startIcon')) 1520 .height(20) 1521 } 1522 }) 1523 } 1524 .columnsTemplate('1fr 1fr 1fr 1fr 1fr') 1525 .rowsTemplate('1fr 1fr 1fr 1fr') 1526 .columnsGap(10) 1527 .width('90%') 1528 .height(160) 1529 } 1530 .margin({ left: 10, right: 10 }) 1531 .backgroundColor(0xFAEEE0) 1532 } 1533} 1534 1535@Reusable 1536@Component 1537struct ChildComponentB { 1538 @State item: string = ''; 1539 1540 aboutToReuse(params: ESObject) { 1541 this.item = params.item; 1542 } 1543 1544 build() { 1545 Row() { 1546 Text(`Item ${this.item} Child Component B`) 1547 .fontSize(20) 1548 .margin({ left: 10 }) 1549 .fontColor(Color.Red) 1550 }.margin({ left: 10, right: 10 }) 1551 } 1552} 1553 1554@Reusable 1555@Component 1556struct ChildComponentC { 1557 @State item: string = ''; 1558 1559 aboutToReuse(params: ESObject) { 1560 this.item = params.item; 1561 } 1562 1563 build() { 1564 Row() { 1565 Text(`Item ${this.item} Child Component C`) 1566 .fontSize(20) 1567 .margin({ left: 10 }) 1568 .fontColor(Color.Green) 1569 }.margin({ left: 10, right: 10 }) 1570 } 1571} 1572 1573@Reusable 1574@Component 1575struct ChildComponentD { 1576 @State item: string = ''; 1577 1578 aboutToReuse(params: ESObject) { 1579 this.item = params.item; 1580 } 1581 1582 build() { 1583 Row() { 1584 Text(`Item ${this.item} Child Component D`) 1585 .fontSize(20) 1586 .margin({ left: 10 }) 1587 .fontColor(Color.Orange) 1588 }.margin({ left: 10, right: 10 }) 1589 } 1590} 1591``` 1592