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