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