1# \@Reusable装饰器:组件复用
2
3
4\@Reusable装饰器装饰任意自定义组件时,表示该自定义组件可以复用。
5
6> **说明:**
7>
8> 从API version 10开始,对\@Reusable进行支持,支持在ArkTS中使用。
9
10
11
12## 概述
13
14- \@Reusable适用自定义组件,与\@Component结合使用,标记为\@Reusable的自定义组件从组件树上被移除时,组件和其对应的JSView对象都会被放入复用缓存中,后续创建新自定义组件节点时,会复用缓存区中的节点,节约组件重新创建的时间。
15
16## 限制条件
17
18- \@Reusable装饰器仅用于自定义组件。
19
20```ts
21import { ComponentContent } from "@kit.ArkUI";
22
23// @Builder加上@Reusable编译报错,不适用于builder
24// @Reusable
25@Builder
26function buildCreativeLoadingDialog(closedClick: () => void) {
27  Crash()
28}
29
30@Component
31export struct Crash {
32  build() {
33    Column() {
34      Text("Crash")
35        .fontSize(12)
36        .lineHeight(18)
37        .fontColor(Color.Blue)
38        .margin({
39          left: 6
40        })
41    }.width('100%')
42    .height('100%')
43    .justifyContent(FlexAlign.Center)
44  }
45}
46
47@Entry
48@Component
49struct Index {
50  @State message: string = 'Hello World';
51  private uicontext = this.getUIContext()
52
53  build() {
54    RelativeContainer() {
55      Text(this.message)
56        .id('Index')
57        .fontSize(50)
58        .fontWeight(FontWeight.Bold)
59        .alignRules({
60          center: { anchor: '__container__', align: VerticalAlign.Center },
61          middle: { anchor: '__container__', align: HorizontalAlign.Center }
62        })
63        .onClick(() => {
64          let contentNode = new ComponentContent(this.uicontext, wrapBuilder(buildCreativeLoadingDialog), () => {
65          });
66          this.uicontext.getPromptAction().openCustomDialog(contentNode);
67        })
68    }
69    .height('100%')
70    .width('100%')
71  }
72}
73```
74
75- ComponentContent不支持传入\@Reusable装饰器装饰的自定义组件。
76
77```ts
78import { ComponentContent } from "@kit.ArkUI";
79@Builder
80function buildCreativeLoadingDialog(closedClick: () => void) {
81  Crash()
82}
83
84// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash
85@Reusable
86@Component
87export struct Crash {
88  build() {
89    Column() {
90      Text("Crash")
91        .fontSize(12)
92        .lineHeight(18)
93        .fontColor(Color.Blue)
94        .margin({
95          left: 6
96        })
97    }.width('100%')
98    .height('100%')
99    .justifyContent(FlexAlign.Center)
100  }
101}
102
103@Entry
104@Component
105struct Index {
106  @State message: string = 'Hello World';
107  private uicontext = this.getUIContext()
108
109  build() {
110    RelativeContainer() {
111      Text(this.message)
112        .id('Index')
113        .fontSize(50)
114        .fontWeight(FontWeight.Bold)
115        .alignRules({
116          center: { anchor: '__container__', align: VerticalAlign.Center },
117          middle: { anchor: '__container__', align: HorizontalAlign.Center }
118        })
119        .onClick(() => {
120          // ComponentContent底层是buildNode,buildNode不支持传入@Reusable注解的自定义组件
121          let contentNode = new ComponentContent(this.uicontext, wrapBuilder(buildCreativeLoadingDialog), () => {
122          });
123          this.uicontext.getPromptAction().openCustomDialog(contentNode);
124        })
125    }
126    .height('100%')
127    .width('100%')
128  }
129}
130```
131
132- \@Reusable装饰器不支持嵌套使用,存在增加内存和不方便维护的问题;
133
134
135> **说明:**
136>
137> 不支持嵌套使用,只是标记,会多增加一个缓存池,各自的复用缓存池存在相同树状结构,复用效率低,引发复用内存增加;
138>
139> 嵌套使用形成各自独立的复用缓存池之后,生命周期的传递存在问题,资源和变量管理无法共享,并不方便维护,容易引发问题;
140>
141> 示例中PlayButton形成的复用缓存池,并不能在PlayButton02的复用缓存池使用,但PlayButton02自己形成复用缓存相互可以使用;
142> 在PlayButton隐藏时已经触发PlayButton02的aboutToRecycle,但是在PlayButton02单独显示时却无法执行aboutToReuse,组件复用的生命周期方法存在无法成对调用问题;
143>
144> 综上,不建议嵌套使用。
145
146
147```ts
148@Entry
149@Component
150struct Index {
151  @State isPlaying: boolean = false;
152  @State isPlaying02: boolean = true;
153  @State isPlaying01: boolean = false;
154
155  build() {
156    Column() {
157      if (this.isPlaying02) {
158
159        // 初始态是显示的按钮
160        Text("Default shown childbutton")
161          .fontSize(14)
162        PlayButton02({ isPlaying02: $isPlaying02 })
163      }
164      Text(`==================`).fontSize(14)
165
166      // 初始态是显示的按钮
167      if (this.isPlaying01) {
168        Text("Default hiden childbutton")
169          .fontSize(14)
170        PlayButton02({ isPlaying02: $isPlaying01 })
171      }
172      Text(`==================`).fontSize(14)
173
174      // 父子嵌套
175      if (this.isPlaying) {
176        Text("parent child 嵌套")
177          .fontSize(14)
178        PlayButton({ buttonPlaying: $isPlaying })
179      }
180      Text(`==================`).fontSize(14);
181
182      // 父子嵌套控制
183      Text(`Parent=child==is ${this.isPlaying ? '' : 'not'} playing`).fontSize(14)
184      Button('Parent=child===controll=' + this.isPlaying)
185        .margin(14)
186        .onClick(() => {
187          this.isPlaying = !this.isPlaying;
188        })
189
190      Text(`==================`).fontSize(14);
191
192      //  默认隐藏按钮控制
193      Text(`hidedchild==is ${this.isPlaying01 ? '' : 'not'} playing`).fontSize(14)
194      Button('Button===hidedchild==control==' + this.isPlaying01)
195        .margin(14)
196        .onClick(() => {
197          this.isPlaying01 = !this.isPlaying01;
198        })
199      Text(`==================`).fontSize(14);
200
201      // 默认显示按钮控制
202      Text(`shownchid==is ${this.isPlaying02 ? '' : 'not'} playing`).fontSize(14)
203      Button('Button===shownchid==control==:' + this.isPlaying02)
204        .margin(15)
205        .onClick(() => {
206          this.isPlaying02 = !this.isPlaying02;
207        })
208    }
209  }
210}
211
212// 复用1
213@Reusable
214@Component
215struct PlayButton {
216  @Link buttonPlaying: boolean;
217
218  build() {
219    Column() {
220
221      // 复用
222      PlayButton02({ isPlaying02: $buttonPlaying })
223      Button(this.buttonPlaying ? 'parent_pause' : 'parent_play')
224        .margin(12)
225        .onClick(() => {
226          this.buttonPlaying = !this.buttonPlaying;
227        })
228    }
229  }
230}
231
232//  复用2 不建议嵌套使用
233@Reusable
234@Component
235struct PlayButton02 {
236  @Link isPlaying02: boolean;
237
238  aboutToRecycle(): void {
239    console.log("=====aboutToRecycle====PlayButton02====");
240  }
241
242  aboutToReuse(params: ESObject): void {
243    console.log("=====aboutToReuse====PlayButton02====");
244  }
245
246  build() {
247    Column() {
248      Button('===commonbutton=====')
249        .margin(12)
250    }
251  }
252}
253```
254
255## 使用场景
256
257- 列表滚动:当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
258
259- 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
260
261- 频繁创建和销毁数据项的视图场景下。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。
262
263
264## 使用场景举例
265
266### 动态布局更新
267
268- 示例代码将Child自定义组件标记为复用组件,通过Button点击更新Child,触发Child复用;
269- \@Reusable:自定义组件被\@Reusable装饰器修饰,即表示其具备组件复用的能力;
270- aboutToReuse:当一个可复用的自定义组件从复用缓存中重新加入到节点树时,触发aboutToReuse生命周期回调,并将组件的构造参数传递给aboutToReuse。
271
272```ts
273// xxx.ets
274export class Message {
275  value: string | undefined;
276
277  constructor(value: string) {
278    this.value = value;
279  }
280}
281
282@Entry
283@Component
284struct Index {
285  @State switch: boolean = true;
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        Child({ message: new Message('Child') })
296          // 如果只有一个复用的组件,可以不用设置reuseId
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### 列表滚动配合LazyForEach使用
327
328- 示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用;
329- \@Reusable:自定义组件被@Reusable装饰器修饰,即表示其具备组件复用的能力;
330- 变量item的被\@State修饰,才能更新,非\@State修饰变量存在无法更新问题。
331
332```ts
333class MyDataSource implements IDataSource {
334  private dataArray: string[] = [];
335  private listener: DataChangeListener | undefined;
336
337  public totalCount(): number {
338    return this.dataArray.length;
339  }
340
341  public getData(index: number): string {
342    return this.dataArray[index];
343  }
344
345  public pushData(data: string): void {
346    this.dataArray.push(data);
347  }
348
349  public reloadListener(): void {
350    this.listener?.onDataReloaded();
351  }
352
353  public registerDataChangeListener(listener: DataChangeListener): void {
354    this.listener = listener;
355  }
356
357  public unregisterDataChangeListener(listener: DataChangeListener): void {
358    this.listener = undefined;
359  }
360}
361
362@Entry
363@Component
364struct ReuseDemo {
365  private data: MyDataSource = new MyDataSource();
366
367  aboutToAppear() {
368    for (let i = 1; i < 1000; i++) {
369      this.data.pushData(i+"");
370    }
371  }
372
373  // ...
374  build() {
375    Column() {
376      List() {
377        LazyForEach(this.data, (item: string) => {
378          ListItem() {
379            CardView({ item: item })
380          }
381        }, (item: string) => item)
382      }
383    }
384  }
385}
386
387// 复用组件
388@Reusable
389@Component
390export struct CardView {
391  @State item: string = '';
392
393  aboutToReuse(params: Record<string, Object>): void {
394    this.item = params.item as string;
395  }
396
397  build() {
398    Column() {
399      Text(this.item)
400        .fontSize(30)
401    }
402    .borderWidth(1)
403    .height(100)
404  }
405}
406```
407
408### if使用场景
409
410- 示例代码将OneMoment自定义组件标记为复用组件,List上下滑动,触发OneMoment复用;
411- 可以使用reuseId为复用组件分配复用组,相同reuseId的组件会在同一个复用组中复用,如果只有一个复用的组件,可以不用设置reuseId;
412- 通过reuseId来标识需要复用的组件,省去重复执行if的删除重创逻辑,提高组件复用的效率和性能。
413
414```ts
415@Entry
416@Component
417struct Index {
418  private dataSource = new MyDataSource<FriendMoment>();
419
420  aboutToAppear(): void {
421    for (let i = 0; i < 20; i++) {
422      let title = i + 1 + "test_if";
423      this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon'))
424    }
425
426    for (let i = 0; i < 50; i++) {
427      let title = i + 1 + "test_if";
428      this.dataSource.pushData(new FriendMoment(i.toString(), title, ''))
429    }
430  }
431
432  build() {
433    Column() {
434      // TopBar()
435      List({ space: 3 }) {
436        LazyForEach(this.dataSource, (moment: FriendMoment) => {
437          ListItem() {
438            OneMoment({ moment: moment })// 使用reuseId进行组件复用的控制
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  // 复用id相同的同才能触发复用
468  aboutToReuse(params: ESObject): void {
469    console.log("=====aboutToReuse====OneMoment==复用了==" + this.moment.text);
470  }
471
472  build() {
473    Column() {
474      Text(this.moment.text)
475      // if分支判断
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- 示例点击update,数据刷新成功,但是滑动列表,组件复用无法使用,Foreach的折叠展开属性的原因;
541- 点击clear,再次update,复用成功;符合一帧内重复创建多个已被销毁的自定义组件。
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    // 点击 update,首次进入,上下滑动,由于Foreach折叠展开属性,无法复用
635    console.log("=====abouTo===Appear=====ListItemView==创建了==" + this.item)
636  }
637
638  aboutToReuse(params: ESObject) {
639    this.item = params.item;
640    // 点击 clear,再次update,复用成功
641    // 符合一帧内重复创建多个已被销毁的自定义组件
642    console.log("=====aboutTo===Reuse====ListItemView==复用了==" + this.item)
643  }
644
645  build() {
646    Column({ space: 10 }) {
647      Text(`${this.obj.id}.标题`)
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- 示例中使用\@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力;
682- 使用aboutToReuse是为了让Grid在滑动时从复用缓存中加入到组件树之前触发,用于更新组件的状态变量以展示正确的内容;
683- 需要注意的是无需在aboutToReuse中对\@Link、\@StorageLink、\@ObjectLink、\@Consume等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。
684
685```ts
686// MyDataSource类实现IDataSource接口
687class MyDataSource implements IDataSource {
688  private dataArray: number[] = [];
689
690  public pushData(data: number): void {
691    this.dataArray.push(data);
692  }
693
694  // 数据源的数据总量
695  public totalCount(): number {
696    return this.dataArray.length;
697  }
698
699  // 返回指定索引位置的数据
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  // 数据源
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            // 使用可复用自定义组件
729            ReusableChildComponent({ item: item })
730          }
731        }, (item: string) => item)
732      }
733      .cachedCount(2) // 设置GridItem的缓存数量
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@Reusable
746@Component
747struct ReusableChildComponent {
748  @State item: number = 0;
749
750  // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
751  // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type
752  aboutToReuse(params: Record<string, number>) {
753    this.item = params.item;
754  }
755
756  build() {
757    Column() {
758      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错
759      Image($r('app.media.app_icon'))
760        .objectFit(ImageFit.Fill)
761        .layoutWeight(1)
762      Text(`图片${this.item}`)
763        .fontSize(16)
764        .textAlign(TextAlign.Center)
765    }
766    .width('100%')
767    .height(120)
768    .backgroundColor(0xF9CF93)
769  }
770}
771```
772
773### WaterFlow使用场景
774
775- WaterFlow滑动场景存在FlowItem及其子组件的频繁创建和销毁,可以将FlowItem中的组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。
776
777```ts
778class WaterFlowDataSource implements IDataSource {
779  private dataArray: number[] = [];
780  private listeners: DataChangeListener[] = [];
781
782  constructor() {
783    for (let i = 0; i <= 60; i++) {
784      this.dataArray.push(i);
785    }
786  }
787
788  // 获取索引对应的数据
789  public getData(index: number): number {
790    return this.dataArray[index];
791  }
792
793  // 通知控制器增加数据
794  notifyDataAdd(index: number): void {
795    this.listeners.forEach(listener => {
796      listener.onDataAdd(index);
797    })
798  }
799
800  // 获取数据总数
801  public totalCount(): number {
802    return this.dataArray.length;
803  }
804
805  // 注册改变数据的控制器
806  registerDataChangeListener(listener: DataChangeListener): void {
807    if (this.listeners.indexOf(listener) < 0) {
808      this.listeners.push(listener);
809    }
810  }
811
812  // 注销改变数据的控制器
813  unregisterDataChangeListener(listener: DataChangeListener): void {
814    const pos = this.listeners.indexOf(listener);
815    if (pos >= 0) {
816      this.listeners.splice(pos, 1);
817    }
818  }
819
820  // 在数据尾部增加一个元素
821  public addLastItem(): void {
822    this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
823    this.notifyDataAdd(this.dataArray.length - 1);
824  }
825}
826
827@Reusable
828@Component
829struct ReusableFlowItem {
830  @State item: number = 0;
831
832  // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
833  aboutToReuse(params: ESObject) {
834    this.item = params.item;
835    console.log("=====aboutToReuse====FlowItem==复用了==" + this.item);
836  }
837
838  aboutToRecycle(): void {
839    console.log("=====aboutToRecycle====FlowItem==回收了==" + this.item);
840  }
841
842  build() {
843    // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错
844    Column() {
845      Text("N" + this.item).fontSize(24).height('26').margin(10)
846      Image($r('app.media.app_icon'))
847        .objectFit(ImageFit.Cover)
848        .width(50)
849        .height(50)
850    }
851  }
852}
853
854@Entry
855@Component
856struct Index {
857  @State minSize: number = 50;
858  @State maxSize: number = 80;
859  @State fontSize: number = 24;
860  @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F];
861  scroller: Scroller = new Scroller();
862  dataSource: WaterFlowDataSource = new WaterFlowDataSource();
863  private itemWidthArray: number[] = [];
864  private itemHeightArray: number[] = [];
865
866  // 计算flow item宽/高
867  getSize() {
868    let ret = Math.floor(Math.random() * this.maxSize);
869    return (ret > this.minSize ? ret : this.minSize);
870  }
871
872  // 保存flow item宽/高
873  getItemSizeArray() {
874    for (let i = 0; i < 100; i++) {
875      this.itemWidthArray.push(this.getSize());
876      this.itemHeightArray.push(this.getSize());
877    }
878  }
879
880  aboutToAppear() {
881    this.getItemSizeArray();
882  }
883
884  build() {
885    Stack({ alignContent: Alignment.TopStart }) {
886      Column({ space: 2 }) {
887        Button('back top')
888          .height('5%')
889          .onClick(() => { // 点击后回到顶部
890            this.scroller.scrollEdge(Edge.Top);
891          })
892        WaterFlow({ scroller: this.scroller }) {
893          LazyForEach(this.dataSource, (item: number) => {
894            FlowItem() {
895              ReusableFlowItem({ item: item })
896            }.onAppear(() => {
897              if (item + 20 == this.dataSource.totalCount()) {
898                for (let i = 0; i < 50; i++) {
899                  this.dataSource.addLastItem();
900                }
901              }
902            })
903
904          })
905        }
906      }
907    }
908  }
909
910  @Builder
911  itemFoot() {
912    Column() {
913      Text(`Footer`)
914        .fontSize(10)
915        .backgroundColor(Color.Red)
916        .width(50)
917        .height(50)
918        .align(Alignment.Center)
919        .margin({ top: 2 })
920    }
921  }
922}
923```
924
925### Swiper使用场景
926
927- Swiper滑动场景,条目中存在子组件的频繁创建和销毁,可以将条目中的子组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。
928
929```ts
930@Entry
931@Component
932struct Index {
933  private dataSource = new MyDataSource<Question>();
934
935  aboutToAppear(): void {
936    for (let i = 0; i < 1000; i++) {
937      let title = i + 1 + "test_swiper";
938      let answers = ["test1", "test2", "test3",
939        "test4"];
940      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错
941      this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers));
942    }
943  }
944
945  build() {
946    Column({ space: 5 }) {
947      Swiper() {
948        LazyForEach(this.dataSource, (item: Question) => {
949          QuestionSwiperItem({ itemData: item })
950        }, (item: Question) => item.id)
951      }
952    }
953    .width('100%')
954    .margin({ top: 5 })
955  }
956}
957
958class Question {
959  id: string = '';
960  title: ResourceStr = '';
961  image: ResourceStr = '';
962  answers: Array<ResourceStr> = [];
963
964  constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) {
965    this.id = id;
966    this.title = title;
967    this.image = image;
968    this.answers = answers;
969  }
970}
971
972@Reusable
973@Component
974struct QuestionSwiperItem {
975  @State itemData: Question | null = null;
976
977  aboutToReuse(params: Record<string, Object>): void {
978    this.itemData = params.itemData as Question;
979    console.info("===test===aboutToReuse====QuestionSwiperItem==");
980  }
981
982  build() {
983    Column() {
984      Text(this.itemData?.title)
985        .fontSize(18)
986        .fontColor($r('sys.color.ohos_id_color_primary'))
987        .alignSelf(ItemAlign.Start)
988        .margin({
989          top: 10,
990          bottom: 16
991        })
992      Image(this.itemData?.image)
993        .width('100%')
994        .borderRadius(12)
995        .objectFit(ImageFit.Contain)
996        .margin({
997          bottom: 16
998        })
999        .height(80)
1000        .width(80)
1001
1002      Column({ space: 16 }) {
1003        ForEach(this.itemData?.answers, (item: Resource) => {
1004          Text(item)
1005            .fontSize(16)
1006            .fontColor($r('sys.color.ohos_id_color_primary'))
1007        }, (item: ResourceStr) => JSON.stringify(item))
1008      }
1009      .width('100%')
1010      .alignItems(HorizontalAlign.Start)
1011    }
1012    .width('100%')
1013    .padding({
1014      left: 16,
1015      right: 16
1016    })
1017  }
1018}
1019
1020class BasicDataSource<T> implements IDataSource {
1021  private listeners: DataChangeListener[] = [];
1022  private originDataArray: T[] = [];
1023
1024  public totalCount(): number {
1025    return 0;
1026  }
1027
1028  public getData(index: number): T {
1029    return this.originDataArray[index];
1030  }
1031
1032  registerDataChangeListener(listener: DataChangeListener): void {
1033    if (this.listeners.indexOf(listener) < 0) {
1034      this.listeners.push(listener);
1035    }
1036  }
1037
1038  unregisterDataChangeListener(listener: DataChangeListener): void {
1039    const pos = this.listeners.indexOf(listener);
1040    if (pos >= 0) {
1041      this.listeners.splice(pos, 1);
1042    }
1043  }
1044
1045  notifyDataAdd(index: number): void {
1046    this.listeners.forEach(listener => {
1047      listener.onDataAdd(index);
1048    })
1049  }
1050}
1051
1052export class MyDataSource<T> extends BasicDataSource<T> {
1053  private dataArray: T[] = [];
1054
1055  public totalCount(): number {
1056    return this.dataArray.length;
1057  }
1058
1059  public getData(index: number): T {
1060    return this.dataArray[index];
1061  }
1062
1063  public pushData(data: T): void {
1064    this.dataArray.push(data);
1065    this.notifyDataAdd(this.dataArray.length - 1);
1066  }
1067}
1068```
1069
1070### ListItemGroup使用场景
1071
1072- 可以视作特殊List滑动场景,将ListItem需要销毁重建的子组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。
1073
1074```ts
1075@Entry
1076@Component
1077struct ListItemGroupAndReusable {
1078  data: DataSrc2 = new DataSrc2();
1079
1080  @Builder
1081  itemHead(text: string) {
1082    Text(text)
1083      .fontSize(20)
1084      .backgroundColor(0xAABBCC)
1085      .width('100%')
1086      .padding(10)
1087  }
1088
1089  aboutToAppear() {
1090    for (let i = 0; i < 10000; i++) {
1091      let data_1 = new DataSrc1();
1092      for (let j = 0; j < 12; j++) {
1093        data_1.Data.push(`测试条目数据: ${i} - ${j}`);
1094      }
1095      this.data.Data.push(data_1);
1096    }
1097  }
1098
1099  build() {
1100    Stack() {
1101      List() {
1102        LazyForEach(this.data, (item: DataSrc1, index: number) => {
1103          ListItemGroup({ header: this.itemHead(index.toString()) }) {
1104            LazyForEach(item, (ii: string, index: number) => {
1105              ListItem() {
1106                Inner({ str: ii });
1107              }
1108            })
1109          }
1110          .width('100%')
1111          .height('60vp')
1112        })
1113      }
1114    }
1115    .width('100%')
1116    .height('100%')
1117  }
1118}
1119
1120@Reusable
1121@Component
1122struct Inner {
1123  @State str: string = ''
1124
1125  aboutToReuse(param: ESObject) {
1126    this.str = param.str;
1127  }
1128
1129  build() {
1130    Text(this.str)
1131  }
1132}
1133
1134class DataSrc1 implements IDataSource {
1135  listeners: DataChangeListener[] = [];
1136  Data: string[] = [];
1137
1138  public totalCount(): number {
1139    return this.Data.length;
1140  }
1141
1142  public getData(index: number): string {
1143    return this.Data[index];
1144  }
1145
1146  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
1147  registerDataChangeListener(listener: DataChangeListener): void {
1148    if (this.listeners.indexOf(listener) < 0) {
1149      this.listeners.push(listener);
1150    }
1151  }
1152
1153  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
1154  unregisterDataChangeListener(listener: DataChangeListener): void {
1155    const pos = this.listeners.indexOf(listener);
1156    if (pos >= 0) {
1157      this.listeners.splice(pos, 1);
1158    }
1159  }
1160
1161  // 通知LazyForEach组件需要重载所有子组件
1162  notifyDataReload(): void {
1163    this.listeners.forEach(listener => {
1164      listener.onDataReloaded();
1165    })
1166  }
1167
1168  // 通知LazyForEach组件需要在index对应索引处添加子组件
1169  notifyDataAdd(index: number): void {
1170    this.listeners.forEach(listener => {
1171      listener.onDataAdd(index);
1172    })
1173  }
1174
1175  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
1176  notifyDataChange(index: number): void {
1177    this.listeners.forEach(listener => {
1178      listener.onDataChange(index);
1179    })
1180  }
1181
1182  // 通知LazyForEach组件需要在index对应索引处删除该子组件
1183  notifyDataDelete(index: number): void {
1184    this.listeners.forEach(listener => {
1185      listener.onDataDelete(index);
1186    })
1187  }
1188
1189  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
1190  notifyDataMove(from: number, to: number): void {
1191    this.listeners.forEach(listener => {
1192      listener.onDataMove(from, to);
1193    })
1194  }
1195}
1196
1197class DataSrc2 implements IDataSource {
1198  listeners: DataChangeListener[] = [];
1199  Data: DataSrc1[] = [];
1200
1201  public totalCount(): number {
1202    return this.Data.length;
1203  }
1204
1205  public getData(index: number): DataSrc1 {
1206    return this.Data[index];
1207  }
1208
1209  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
1210  registerDataChangeListener(listener: DataChangeListener): void {
1211    if (this.listeners.indexOf(listener) < 0) {
1212      this.listeners.push(listener);
1213    }
1214  }
1215
1216  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
1217  unregisterDataChangeListener(listener: DataChangeListener): void {
1218    const pos = this.listeners.indexOf(listener);
1219    if (pos >= 0) {
1220      this.listeners.splice(pos, 1);
1221    }
1222  }
1223
1224  // 通知LazyForEach组件需要重载所有子组件
1225  notifyDataReload(): void {
1226    this.listeners.forEach(listener => {
1227      listener.onDataReloaded();
1228    })
1229  }
1230
1231  // 通知LazyForEach组件需要在index对应索引处添加子组件
1232  notifyDataAdd(index: number): void {
1233    this.listeners.forEach(listener => {
1234      listener.onDataAdd(index);
1235    })
1236  }
1237
1238  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
1239  notifyDataChange(index: number): void {
1240    this.listeners.forEach(listener => {
1241      listener.onDataChange(index);
1242    })
1243  }
1244
1245  // 通知LazyForEach组件需要在index对应索引处删除该子组件
1246  notifyDataDelete(index: number): void {
1247    this.listeners.forEach(listener => {
1248      listener.onDataDelete(index);
1249    })
1250  }
1251
1252  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
1253  notifyDataMove(from: number, to: number): void {
1254    this.listeners.forEach(listener => {
1255      listener.onDataMove(from, to);
1256    })
1257  }
1258}
1259```
1260
1261
1262### 多种条目类型使用场景
1263
1264#### 标准型
1265
1266- 复用组件之间布局完全相同;
1267- 示例同列表滚动中描述;
1268
1269#### 有限变化型
1270
1271- 复用组件之间有不同,但是类型有限;
1272- 示例为复用组件显式设置两个reuseId与使用两个自定义组件进行复用;
1273
1274```ts
1275class MyDataSource implements IDataSource {
1276  private dataArray: string[] = [];
1277  private listener: DataChangeListener | undefined;
1278
1279  public totalCount(): number {
1280    return this.dataArray.length;
1281  }
1282
1283  public getData(index: number): string {
1284    return this.dataArray[index];
1285  }
1286
1287  public pushData(data: string): void {
1288    this.dataArray.push(data);
1289  }
1290
1291  public reloadListener(): void {
1292    this.listener?.onDataReloaded();
1293  }
1294
1295  public registerDataChangeListener(listener: DataChangeListener): void {
1296    this.listener = listener;
1297  }
1298
1299  public unregisterDataChangeListener(listener: DataChangeListener): void {
1300    this.listener = undefined;
1301  }
1302}
1303
1304@Entry
1305@Component
1306struct Index {
1307  private data: MyDataSource = new MyDataSource();
1308
1309  aboutToAppear() {
1310    for (let i = 0; i < 1000; i++) {
1311      this.data.pushData(i+"");
1312    }
1313  }
1314
1315  build() {
1316    Column() {
1317      List({ space: 10 }) {
1318        LazyForEach(this.data, (item: number) => {
1319          ListItem() {
1320            ReusableComponent({ item: item })
1321              .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
1322          }
1323          .backgroundColor(Color.Orange)
1324          .width('100%')
1325        }, (item: number) => item.toString())
1326      }
1327      .cachedCount(2)
1328    }
1329  }
1330}
1331
1332@Reusable
1333@Component
1334struct ReusableComponent {
1335  @State item: number = 0;
1336
1337  aboutToReuse(params: ESObject) {
1338    this.item = params.item;
1339  }
1340
1341  build() {
1342    Column() {
1343      if (this.item % 2 === 0) {
1344        Text(`Item ${this.item} ReusableComponentOne`)
1345          .fontSize(20)
1346          .margin({ left: 10 })
1347      } else {
1348        Text(`Item ${this.item} ReusableComponentTwo`)
1349          .fontSize(20)
1350          .margin({ left: 10 })
1351      }
1352    }.margin({ left: 10, right: 10 })
1353  }
1354}
1355
1356```
1357
1358#### 组合型
1359
1360- 复用组件之间有不同,情况非常多,但是拥有共同的子组件;
1361- 示例按照组合型的组件复用方式,将上述示例中的三种复用组件转变为Builder函数后,内部共同的子组件就处于同一个父组件MyComponent下;
1362- 对这些子组件使用组件复用时,它们的缓存池也会在父组件上共享,节省组件创建时的消耗。
1363
1364```ts
1365class MyDataSource implements IDataSource {
1366  private dataArray: string[] = [];
1367  private listener: DataChangeListener | undefined;
1368
1369  public totalCount(): number {
1370    return this.dataArray.length;
1371  }
1372
1373  public getData(index: number): string {
1374    return this.dataArray[index];
1375  }
1376
1377  public pushData(data: string): void {
1378    this.dataArray.push(data);
1379  }
1380
1381  public reloadListener(): void {
1382    this.listener?.onDataReloaded();
1383  }
1384
1385  public registerDataChangeListener(listener: DataChangeListener): void {
1386    this.listener = listener;
1387  }
1388
1389  public unregisterDataChangeListener(listener: DataChangeListener): void {
1390    this.listener = undefined;
1391  }
1392}
1393
1394@Entry
1395@Component
1396struct MyComponent {
1397  private data: MyDataSource = new MyDataSource();
1398
1399  aboutToAppear() {
1400    for (let i = 0; i < 1000; i++) {
1401      this.data.pushData(i.toString())
1402    }
1403  }
1404
1405  @Builder
1406  itemBuilderOne(item: string) {
1407    Column() {
1408      ChildComponentA({ item: item })
1409      ChildComponentB({ item: item })
1410      ChildComponentC({ item: item })
1411    }
1412  }
1413
1414  @Builder
1415  itemBuilderTwo(item: string) {
1416    Column() {
1417      ChildComponentA({ item: item })
1418      ChildComponentC({ item: item })
1419      ChildComponentD({ item: item })
1420    }
1421  }
1422
1423  @Builder
1424  itemBuilderThree(item: string) {
1425    Column() {
1426      ChildComponentA({ item: item })
1427      ChildComponentB({ item: item })
1428      ChildComponentD({ item: item })
1429    }
1430  }
1431
1432  build() {
1433    List({ space: 40 }) {
1434      LazyForEach(this.data, (item: string, index: number) => {
1435        ListItem() {
1436          if (index % 3 === 0) {
1437            this.itemBuilderOne(item)
1438          } else if (index % 5 === 0) {
1439            this.itemBuilderTwo(item)
1440          } else {
1441            this.itemBuilderThree(item)
1442          }
1443        }
1444        .backgroundColor('#cccccc')
1445        .width('100%')
1446        .onAppear(() => {
1447          console.log(`ListItem ${index} onAppear`);
1448        })
1449      }, (item: number) => item.toString())
1450    }
1451    .width('100%')
1452    .height('100%')
1453    .cachedCount(0)
1454  }
1455}
1456
1457@Reusable
1458@Component
1459struct ChildComponentA {
1460  @State item: string = '';
1461
1462  aboutToReuse(params: ESObject) {
1463    console.log(`ChildComponentA ${params.item} Reuse ${this.item}`);
1464    this.item = params.item;
1465  }
1466
1467  aboutToRecycle(): void {
1468    console.log(`ChildComponentA ${this.item} Recycle`);
1469  }
1470
1471  build() {
1472    Column() {
1473      Text(`Item ${this.item} Child Component A`)
1474        .fontSize(20)
1475        .margin({ left: 10 })
1476        .fontColor(Color.Blue)
1477      Grid() {
1478        ForEach((new Array(20)).fill(''), (item: string,index: number) => {
1479          GridItem() {
1480            // 请开发者自行在src/main/resources/base/media路径下添加app.media.startIcon图片,否则运行时会因资源缺失而报错
1481            Image($r('app.media.startIcon'))
1482              .height(20)
1483          }
1484        })
1485      }
1486      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
1487      .rowsTemplate('1fr 1fr 1fr 1fr')
1488      .columnsGap(10)
1489      .width('90%')
1490      .height(160)
1491    }
1492    .margin({ left: 10, right: 10 })
1493    .backgroundColor(0xFAEEE0)
1494  }
1495}
1496
1497@Reusable
1498@Component
1499struct ChildComponentB {
1500  @State item: string = '';
1501
1502  aboutToReuse(params: ESObject) {
1503    this.item = params.item;
1504  }
1505
1506  build() {
1507    Row() {
1508      Text(`Item ${this.item} Child Component B`)
1509        .fontSize(20)
1510        .margin({ left: 10 })
1511        .fontColor(Color.Red)
1512    }.margin({ left: 10, right: 10 })
1513  }
1514}
1515
1516@Reusable
1517@Component
1518struct ChildComponentC {
1519  @State item: string = '';
1520
1521  aboutToReuse(params: ESObject) {
1522    this.item = params.item;
1523  }
1524
1525  build() {
1526    Row() {
1527      Text(`Item ${this.item} Child Component C`)
1528        .fontSize(20)
1529        .margin({ left: 10 })
1530        .fontColor(Color.Green)
1531    }.margin({ left: 10, right: 10 })
1532  }
1533}
1534
1535@Reusable
1536@Component
1537struct ChildComponentD {
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 D`)
1547        .fontSize(20)
1548        .margin({ left: 10 })
1549        .fontColor(Color.Orange)
1550    }.margin({ left: 10, right: 10 })
1551  }
1552}
1553```
1554