• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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