• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
80@Builder
81function buildCreativeLoadingDialog(closedClick: () => void) {
82  Crash()
83}
84
85// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash
86@Reusable
87@Component
88export struct Crash {
89  build() {
90    Column() {
91      Text("Crash")
92        .fontSize(12)
93        .lineHeight(18)
94        .fontColor(Color.Blue)
95        .margin({
96          left: 6
97        })
98    }.width('100%')
99    .height('100%')
100    .justifyContent(FlexAlign.Center)
101  }
102}
103
104@Entry
105@Component
106struct Index {
107  @State message: string = 'Hello World';
108  private uicontext = this.getUIContext();
109
110  build() {
111    RelativeContainer() {
112      Text(this.message)
113        .id('Index')
114        .fontSize(50)
115        .fontWeight(FontWeight.Bold)
116        .alignRules({
117          center: { anchor: '__container__', align: VerticalAlign.Center },
118          middle: { anchor: '__container__', align: HorizontalAlign.Center }
119        })
120        .onClick(() => {
121          // ComponentContent底层是buildNode,buildNode不支持传入@Reusable注解的自定义组件
122          let contentNode = new ComponentContent(this.uicontext, wrapBuilder(buildCreativeLoadingDialog), () => {
123          });
124          this.uicontext.getPromptAction().openCustomDialog(contentNode);
125        })
126    }
127    .height('100%')
128    .width('100%')
129  }
130}
131```
132
133- \@Reusable装饰器不支持嵌套使用,存在增加内存和不方便维护的问题;
134
135
136> **说明:**
137>
138> 不支持嵌套使用,只是标记,会多增加一个缓存池,各自的复用缓存池存在相同树状结构,复用效率低,引发复用内存增加;
139>
140> 嵌套使用形成各自独立的复用缓存池之后,生命周期的传递存在问题,资源和变量管理无法共享,并不方便维护,容易引发问题;
141>
142> 示例中PlayButton形成的复用缓存池,并不能在PlayButton02的复用缓存池使用,但PlayButton02自己形成复用缓存相互可以使用;
143> 在PlayButton隐藏时已经触发PlayButton02的aboutToRecycle,但是在PlayButton02单独显示时却无法执行aboutToReuse,组件复用的生命周期方法存在无法成对调用问题;
144>
145> 综上,不建议嵌套使用。
146
147
148```ts
149@Entry
150@Component
151struct Index {
152  @State isPlaying: boolean = false;
153  @State isPlaying02: boolean = true;
154  @State isPlaying01: boolean = false;
155
156  build() {
157    Column() {
158      if (this.isPlaying02) {
159
160        // 初始态是显示的按钮
161        Text("Default shown childbutton")
162          .fontSize(14)
163        PlayButton02({ isPlaying02: $isPlaying02 })
164      }
165      Text(`------------------------`)
166
167      // 初始态是隐藏的按钮
168      if (this.isPlaying01) {
169        Text("Default hidden childbutton")
170          .fontSize(14)
171        PlayButton02({ isPlaying02: $isPlaying01 })
172      }
173      Text(`------------------------`)
174
175      // 父子嵌套
176      if (this.isPlaying) {
177        Text("parent child 嵌套")
178          .fontSize(14)
179        PlayButton({ buttonPlaying: $isPlaying })
180      }
181      Text(`------------------------`)
182
183      // 父子嵌套控制
184      Text(`Parent=child==is ${this.isPlaying ? '' : 'not'} playing`).fontSize(14)
185      Button('Parent=child===controll=' + this.isPlaying)
186        .margin(14)
187        .onClick(() => {
188          this.isPlaying = !this.isPlaying;
189        })
190
191      Text(`------------------------`)
192
193      //  默认隐藏按钮控制
194      Text(`Hiddenchild==is ${this.isPlaying01 ? '' : 'not'} playing`).fontSize(14)
195      Button('Button===hiddenchild==control==' + this.isPlaying01)
196        .margin(14)
197        .onClick(() => {
198          this.isPlaying01 = !this.isPlaying01;
199        })
200      Text(`------------------------`)
201
202      // 默认显示按钮控制
203      Text(`shownchid==is ${this.isPlaying02 ? '' : 'not'} playing`).fontSize(14)
204      Button('Button===shownchid==control==:' + this.isPlaying02)
205        .margin(15)
206        .onClick(() => {
207          this.isPlaying02 = !this.isPlaying02;
208        })
209    }
210  }
211}
212
213// 复用1
214@Reusable
215@Component
216struct PlayButton {
217  @Link buttonPlaying: boolean;
218
219  build() {
220    Column() {
221
222      // 复用
223      PlayButton02({ isPlaying02: $buttonPlaying })
224      Button(this.buttonPlaying ? 'parent_pause' : 'parent_play')
225        .margin(12)
226        .onClick(() => {
227          this.buttonPlaying = !this.buttonPlaying;
228        })
229    }
230  }
231}
232
233//  复用2 不建议嵌套使用
234@Reusable
235@Component
236struct PlayButton02 {
237  @Link isPlaying02: boolean;
238
239  aboutToRecycle(): void {
240    console.log("=====aboutToRecycle====PlayButton02====");
241  }
242
243  aboutToReuse(params: ESObject): void {
244    console.log("=====aboutToReuse====PlayButton02====");
245  }
246
247  build() {
248    Column() {
249      Button('===commonbutton=====')
250        .margin(12)
251    }
252  }
253}
254```
255
256## 使用场景
257
258- 列表滚动:当应用需要展示大量数据的列表,并且用户进行滚动操作时,频繁创建和销毁列表项的视图可能导致卡顿和性能问题。在这种情况下,使用列表组件的组件复用机制可以重用已经创建的列表项视图,提高滚动的流畅度。
259
260- 动态布局更新:如果应用中的界面需要频繁地进行布局更新,例如根据用户的操作或数据变化动态改变视图结构和样式,重复创建和销毁视图可能导致频繁的布局计算,影响帧率。在这种情况下,使用组件复用可以避免不必要的视图创建和布局计算,提高性能。
261
262- 频繁创建和销毁数据项的视图场景下。使用组件复用可以重用已创建的视图,只更新数据的内容,减少视图的创建和销毁,能有效提高性能。
263
264
265## 使用场景举例
266
267### 动态布局更新
268
269- 示例代码将Child自定义组件标记为复用组件,通过Button点击更新Child,触发Child复用;
270- \@Reusable:自定义组件被\@Reusable装饰器修饰,即表示其具备组件复用的能力;
271- aboutToReuse:当一个可复用的自定义组件从复用缓存中重新加入到节点树时,触发aboutToReuse生命周期回调,并将组件的构造参数传递给aboutToReuse。
272
273```ts
274// xxx.ets
275export class Message {
276  value: string | undefined;
277
278  constructor(value: string) {
279    this.value = value;
280  }
281}
282
283@Entry
284@Component
285struct Index {
286  @State switch: boolean = true;
287
288  build() {
289    Column() {
290      Button('Hello')
291        .fontSize(30)
292        .fontWeight(FontWeight.Bold)
293        .onClick(() => {
294          this.switch = !this.switch;
295        })
296      if (this.switch) {
297        // 如果只有一个复用的组件,可以不用设置reuseId
298        Child({ message: new Message('Child') })
299          .reuseId('Child')
300      }
301    }
302    .height("100%")
303    .width('100%')
304  }
305}
306
307@Reusable
308@Component
309struct Child {
310  @State message: Message = new Message('AboutToReuse');
311
312  aboutToReuse(params: Record<string, ESObject>) {
313    console.info("Recycle ====Child==");
314    this.message = params.message as Message;
315  }
316
317  build() {
318    Column() {
319      Text(this.message.value)
320        .fontSize(30)
321    }
322    .borderWidth(1)
323    .height(100)
324  }
325}
326```
327
328### 列表滚动配合LazyForEach使用
329
330- 示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用;
331- 被\@State修饰的变量item才能更新,未被\@State修饰的变量不会更新。
332
333```ts
334class MyDataSource implements IDataSource {
335  private dataArray: string[] = [];
336  private listener: DataChangeListener | undefined;
337
338  public totalCount(): number {
339    return this.dataArray.length;
340  }
341
342  public getData(index: number): string {
343    return this.dataArray[index];
344  }
345
346  public pushData(data: string): void {
347    this.dataArray.push(data);
348  }
349
350  public reloadListener(): void {
351    this.listener?.onDataReloaded();
352  }
353
354  public registerDataChangeListener(listener: DataChangeListener): void {
355    this.listener = listener;
356  }
357
358  public unregisterDataChangeListener(listener: DataChangeListener): void {
359    this.listener = undefined;
360  }
361}
362
363@Entry
364@Component
365struct ReuseDemo {
366  private data: MyDataSource = new MyDataSource();
367
368  aboutToAppear() {
369    for (let i = 1; i < 1000; i++) {
370      this.data.pushData(i + "");
371    }
372  }
373
374  // ...
375  build() {
376    Column() {
377      List() {
378        LazyForEach(this.data, (item: string) => {
379          ListItem() {
380            CardView({ item: item })
381          }
382        }, (item: string) => item)
383      }
384    }
385  }
386}
387
388// 复用组件
389@Reusable
390@Component
391export struct CardView {
392  @State item: string = '';
393
394  aboutToReuse(params: Record<string, Object>): void {
395    this.item = params.item as string;
396  }
397
398  build() {
399    Column() {
400      Text(this.item)
401        .fontSize(30)
402    }
403    .borderWidth(1)
404    .height(100)
405  }
406}
407```
408
409### if使用场景
410
411- 示例代码将OneMoment自定义组件标记为复用组件,List上下滑动,触发OneMoment复用;
412- 可以使用reuseId为复用组件分配复用组,相同reuseId的组件会在同一个复用组中复用,如果只有一个复用的组件,可以不用设置reuseId;
413- 通过reuseId来标识需要复用的组件,省去重复执行if的删除重创逻辑,提高组件复用的效率和性能。
414
415```ts
416@Entry
417@Component
418struct Index {
419  private dataSource = new MyDataSource<FriendMoment>();
420
421  aboutToAppear(): void {
422    for (let i = 0; i < 20; i++) {
423      let title = i + 1 + "test_if";
424      this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon'));
425    }
426
427    for (let i = 0; i < 50; i++) {
428      let title = i + 1 + "test_if";
429      this.dataSource.pushData(new FriendMoment(i.toString(), title, ''));
430    }
431  }
432
433  build() {
434    Column() {
435      // TopBar()
436      List({ space: 3 }) {
437        LazyForEach(this.dataSource, (moment: FriendMoment) => {
438          ListItem() {
439            // 使用reuseId进行组件复用的控制
440            OneMoment({ moment: moment })
441              .reuseId((moment.image !== '') ? 'withImage' : 'noImage')
442          }
443        }, (moment: FriendMoment) => moment.id)
444      }
445      .cachedCount(0)
446    }
447  }
448}
449
450class FriendMoment {
451  id: string = '';
452  text: string = '';
453  title: string = '';
454  image: string = '';
455  answers: Array<ResourceStr> = [];
456
457  constructor(id: string, title: string, image: string) {
458    this.text = id;
459    this.title = title;
460    this.image = image;
461  }
462}
463
464@Reusable
465@Component
466export struct OneMoment {
467  @Prop moment: FriendMoment;
468
469  // 复用id相同的同才能触发复用
470  aboutToReuse(params: ESObject): void {
471    console.log("=====aboutToReuse====OneMoment==复用了==" + this.moment.text);
472  }
473
474  build() {
475    Column() {
476      Text(this.moment.text)
477      // if分支判断
478      if (this.moment.image !== '') {
479        Flex({ wrap: FlexWrap.Wrap }) {
480          Image($r(this.moment.image)).height(50).width(50)
481          Image($r(this.moment.image)).height(50).width(50)
482          Image($r(this.moment.image)).height(50).width(50)
483          Image($r(this.moment.image)).height(50).width(50)
484        }
485      }
486    }
487  }
488}
489
490class BasicDataSource<T> implements IDataSource {
491  private listeners: DataChangeListener[] = [];
492  private originDataArray: T[] = [];
493
494  public totalCount(): number {
495    return 0;
496  }
497
498  public getData(index: number): T {
499    return this.originDataArray[index];
500  }
501
502  registerDataChangeListener(listener: DataChangeListener): void {
503    if (this.listeners.indexOf(listener) < 0) {
504      this.listeners.push(listener);
505    }
506  }
507
508  unregisterDataChangeListener(listener: DataChangeListener): void {
509    const pos = this.listeners.indexOf(listener);
510    if (pos >= 0) {
511      this.listeners.splice(pos, 1);
512    }
513  }
514
515  notifyDataAdd(index: number): void {
516    this.listeners.forEach(listener => {
517      listener.onDataAdd(index);
518    });
519  }
520}
521
522export class MyDataSource<T> extends BasicDataSource<T> {
523  private dataArray: T[] = [];
524
525  public totalCount(): number {
526    return this.dataArray.length;
527  }
528
529  public getData(index: number): T {
530    return this.dataArray[index];
531  }
532
533  public pushData(data: T): void {
534    this.dataArray.push(data);
535    this.notifyDataAdd(this.dataArray.length - 1);
536  }
537}
538```
539
540### Foreach使用场景
541
542- 使用Foreach创建可复用的自定义组件,由于Foreach渲染控制语法的全展开属性,导致复用组件无法复用;如下示例点击update,数据刷新成功,但是滑动列表,ListItemView无法复用;
543- 点击clear,再次点击update,ListItemView复用成功,因为一帧内重复创建多个已被销毁的自定义组件。
544
545```ts
546// xxx.ets
547class MyDataSource implements IDataSource {
548  private dataArray: string[] = [];
549
550  public totalCount(): number {
551    return this.dataArray.length;
552  }
553
554  public getData(index: number): string {
555    return this.dataArray[index];
556  }
557
558  public pushData(data: string): void {
559    this.dataArray.push(data);
560  }
561
562  public registerDataChangeListener(listener: DataChangeListener): void {
563  }
564
565  public unregisterDataChangeListener(listener: DataChangeListener): void {
566  }
567}
568
569@Entry
570@Component
571struct Index {
572  private data: MyDataSource = new MyDataSource();
573  private data02: MyDataSource = new MyDataSource();
574  @State isShow: boolean = true;
575  @State dataSource: ListItemObject[] = [];
576
577  aboutToAppear() {
578    for (let i = 0; i < 100; i++) {
579      this.data.pushData(i.toString());
580    }
581
582    for (let i = 30; i < 80; i++) {
583      this.data02.pushData(i.toString());
584    }
585  }
586
587  build() {
588    Column() {
589      Row() {
590        Button('clear').onClick(() => {
591          for (let i = 1; i < 50; i++) {
592            let obj = new ListItemObject();
593            obj.id = i;
594            obj.uuid = Math.random().toString();
595            obj.isExpand = false;
596            this.dataSource.pop();
597          }
598        }).height(40)
599
600        Button('update').onClick(() => {
601          for (let i = 1; i < 50; i++) {
602            let obj = new ListItemObject();
603            obj.id = i;
604            obj.uuid = Math.random().toString();
605            obj.isExpand = false;
606            this.dataSource.push(obj);
607          }
608        }).height(40)
609      }
610
611      List({ space: 10 }) {
612        ForEach(this.dataSource, (item: ListItemObject) => {
613          ListItem() {
614            ListItemView({
615              obj: item
616            })
617          }
618        }, (item: ListItemObject) => {
619          return item.uuid.toString();
620        })
621
622      }.cachedCount(0)
623      .width('100%')
624      .height('100%')
625    }
626  }
627}
628
629@Reusable
630@Component
631struct ListItemView {
632  @ObjectLink obj: ListItemObject;
633  @State item: string = '';
634
635  aboutToAppear(): void {
636    // 点击 update,首次进入,上下滑动,由于Foreach折叠展开属性,无法复用
637    console.log("=====aboutToAppear=====ListItemView==创建了==" + this.item);
638  }
639
640  aboutToReuse(params: ESObject) {
641    this.item = params.item;
642    // 点击 clear,再次update,复用成功
643    // 符合一帧内重复创建多个已被销毁的自定义组件
644    console.log("=====aboutToReuse====ListItemView==复用了==" + this.item);
645  }
646
647  build() {
648    Column({ space: 10 }) {
649      Text(`${this.obj.id}.标题`)
650        .fontSize(16)
651        .fontColor('#000000')
652        .padding({
653          top: 20,
654          bottom: 20,
655        })
656
657      if (this.obj.isExpand) {
658        Text('')
659          .fontSize(14)
660          .fontColor('#999999')
661      }
662    }
663    .width('100%')
664    .borderRadius(10)
665    .backgroundColor(Color.White)
666    .padding(15)
667    .onClick(() => {
668      this.obj.isExpand = !this.obj.isExpand;
669    })
670  }
671}
672
673@Observed
674class ListItemObject {
675  uuid: string = "";
676  id: number = 0;
677  isExpand: boolean = false;
678}
679```
680
681### Grid使用场景
682
683- 示例中使用\@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力;
684- 使用aboutToReuse是为了让Grid在滑动时从复用缓存中加入到组件树之前触发,用于更新组件的状态变量以展示正确的内容;
685- 需要注意的是无需在aboutToReuse中对\@Link、\@StorageLink、\@ObjectLink、\@Consume等自动更新值的状态变量进行更新,可能触发不必要的组件刷新。
686
687```ts
688// MyDataSource类实现IDataSource接口
689class MyDataSource implements IDataSource {
690  private dataArray: number[] = [];
691
692  public pushData(data: number): void {
693    this.dataArray.push(data);
694  }
695
696  // 数据源的数据总量
697  public totalCount(): number {
698    return this.dataArray.length;
699  }
700
701  // 返回指定索引位置的数据
702  public getData(index: number): number {
703    return this.dataArray[index];
704  }
705
706  registerDataChangeListener(listener: DataChangeListener): void {
707  }
708
709  unregisterDataChangeListener(listener: DataChangeListener): void {
710  }
711}
712
713@Entry
714@Component
715struct MyComponent {
716  // 数据源
717  private data: MyDataSource = new MyDataSource();
718
719  aboutToAppear() {
720    for (let i = 1; i < 1000; i++) {
721      this.data.pushData(i);
722    }
723  }
724
725  build() {
726    Column({ space: 5 }) {
727      Grid() {
728        LazyForEach(this.data, (item: number) => {
729          GridItem() {
730            // 使用可复用自定义组件
731            ReusableChildComponent({ item: item })
732          }
733        }, (item: string) => item)
734      }
735      .cachedCount(2) // 设置GridItem的缓存数量
736      .columnsTemplate('1fr 1fr 1fr')
737      .columnsGap(10)
738      .rowsGap(10)
739      .margin(10)
740      .height(500)
741      .backgroundColor(0xFAEEE0)
742    }
743  }
744}
745
746@Reusable
747@Component
748struct ReusableChildComponent {
749  @State item: number = 0;
750
751  // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
752  // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type
753  aboutToReuse(params: Record<string, number>) {
754    this.item = params.item;
755  }
756
757  build() {
758    Column() {
759      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错
760      Image($r('app.media.app_icon'))
761        .objectFit(ImageFit.Fill)
762        .layoutWeight(1)
763      Text(`图片${this.item}`)
764        .fontSize(16)
765        .textAlign(TextAlign.Center)
766    }
767    .width('100%')
768    .height(120)
769    .backgroundColor(0xF9CF93)
770  }
771}
772```
773
774### WaterFlow使用场景
775
776- WaterFlow滑动场景存在FlowItem及其子组件的频繁创建和销毁,可以将FlowItem中的组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。
777
778```ts
779class WaterFlowDataSource implements IDataSource {
780  private dataArray: number[] = [];
781  private listeners: DataChangeListener[] = [];
782
783  constructor() {
784    for (let i = 0; i <= 60; i++) {
785      this.dataArray.push(i);
786    }
787  }
788
789  // 获取索引对应的数据
790  public getData(index: number): number {
791    return this.dataArray[index];
792  }
793
794  // 通知控制器增加数据
795  notifyDataAdd(index: number): void {
796    this.listeners.forEach(listener => {
797      listener.onDataAdd(index);
798    });
799  }
800
801  // 获取数据总数
802  public totalCount(): number {
803    return this.dataArray.length;
804  }
805
806  // 注册改变数据的控制器
807  registerDataChangeListener(listener: DataChangeListener): void {
808    if (this.listeners.indexOf(listener) < 0) {
809      this.listeners.push(listener);
810    }
811  }
812
813  // 注销改变数据的控制器
814  unregisterDataChangeListener(listener: DataChangeListener): void {
815    const pos = this.listeners.indexOf(listener);
816    if (pos >= 0) {
817      this.listeners.splice(pos, 1);
818    }
819  }
820
821  // 在数据尾部增加一个元素
822  public addLastItem(): void {
823    this.dataArray.splice(this.dataArray.length, 0, this.dataArray.length);
824    this.notifyDataAdd(this.dataArray.length - 1);
825  }
826}
827
828@Reusable
829@Component
830struct ReusableFlowItem {
831  @State item: number = 0;
832
833  // 从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容
834  aboutToReuse(params: ESObject) {
835    this.item = params.item;
836    console.log("=====aboutToReuse====FlowItem==复用了==" + this.item);
837  }
838
839  aboutToRecycle(): void {
840    console.log("=====aboutToRecycle====FlowItem==回收了==" + this.item);
841  }
842
843  build() {
844    // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错
845    Column() {
846      Text("N" + this.item).fontSize(24).height('26').margin(10)
847      Image($r('app.media.app_icon'))
848        .objectFit(ImageFit.Cover)
849        .width(50)
850        .height(50)
851    }
852  }
853}
854
855@Entry
856@Component
857struct Index {
858  @State minSize: number = 50;
859  @State maxSize: number = 80;
860  @State fontSize: number = 24;
861  @State colors: number[] = [0xFFC0CB, 0xDA70D6, 0x6B8E23, 0x6A5ACD, 0x00FFFF, 0x00FF7F];
862  scroller: Scroller = new Scroller();
863  dataSource: WaterFlowDataSource = new WaterFlowDataSource();
864  private itemWidthArray: number[] = [];
865  private itemHeightArray: number[] = [];
866
867  // 计算flow item宽/高
868  getSize() {
869    let ret = Math.floor(Math.random() * this.maxSize);
870    return (ret > this.minSize ? ret : this.minSize);
871  }
872
873  // 保存flow item宽/高
874  getItemSizeArray() {
875    for (let i = 0; i < 100; i++) {
876      this.itemWidthArray.push(this.getSize());
877      this.itemHeightArray.push(this.getSize());
878    }
879  }
880
881  aboutToAppear() {
882    this.getItemSizeArray();
883  }
884
885  build() {
886    Stack({ alignContent: Alignment.TopStart }) {
887      Column({ space: 2 }) {
888        Button('back top')
889          .height('5%')
890          .onClick(() => { // 点击后回到顶部
891            this.scroller.scrollEdge(Edge.Top);
892          })
893        WaterFlow({ scroller: this.scroller }) {
894          LazyForEach(this.dataSource, (item: number) => {
895            FlowItem() {
896              ReusableFlowItem({ item: item })
897            }.onAppear(() => {
898              if (item + 20 == this.dataSource.totalCount()) {
899                for (let i = 0; i < 50; i++) {
900                  this.dataSource.addLastItem();
901                }
902              }
903            })
904
905          })
906        }
907      }
908    }
909  }
910
911  @Builder
912  itemFoot() {
913    Column() {
914      Text(`Footer`)
915        .fontSize(10)
916        .backgroundColor(Color.Red)
917        .width(50)
918        .height(50)
919        .align(Alignment.Center)
920        .margin({ top: 2 })
921    }
922  }
923}
924```
925
926### Swiper使用场景
927
928- Swiper滑动场景,条目中存在子组件的频繁创建和销毁,可以将条目中的子组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。
929
930```ts
931@Entry
932@Component
933struct Index {
934  private dataSource = new MyDataSource<Question>();
935
936  aboutToAppear(): void {
937    for (let i = 0; i < 1000; i++) {
938      let title = i + 1 + "test_swiper";
939      let answers = ["test1", "test2", "test3",
940        "test4"];
941      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错
942      this.dataSource.pushData(new Question(i.toString(), title, $r('app.media.app_icon'), answers));
943    }
944  }
945
946  build() {
947    Column({ space: 5 }) {
948      Swiper() {
949        LazyForEach(this.dataSource, (item: Question) => {
950          QuestionSwiperItem({ itemData: item })
951        }, (item: Question) => item.id)
952      }
953    }
954    .width('100%')
955    .margin({ top: 5 })
956  }
957}
958
959class Question {
960  id: string = '';
961  title: ResourceStr = '';
962  image: ResourceStr = '';
963  answers: Array<ResourceStr> = [];
964
965  constructor(id: string, title: ResourceStr, image: ResourceStr, answers: Array<ResourceStr>) {
966    this.id = id;
967    this.title = title;
968    this.image = image;
969    this.answers = answers;
970  }
971}
972
973@Reusable
974@Component
975struct QuestionSwiperItem {
976  @State itemData: Question | null = null;
977
978  aboutToReuse(params: Record<string, Object>): void {
979    this.itemData = params.itemData as Question;
980    console.info("===test===aboutToReuse====QuestionSwiperItem==");
981  }
982
983  build() {
984    Column() {
985      Text(this.itemData?.title)
986        .fontSize(18)
987        .fontColor($r('sys.color.ohos_id_color_primary'))
988        .alignSelf(ItemAlign.Start)
989        .margin({
990          top: 10,
991          bottom: 16
992        })
993      Image(this.itemData?.image)
994        .width('100%')
995        .borderRadius(12)
996        .objectFit(ImageFit.Contain)
997        .margin({
998          bottom: 16
999        })
1000        .height(80)
1001        .width(80)
1002
1003      Column({ space: 16 }) {
1004        ForEach(this.itemData?.answers, (item: Resource) => {
1005          Text(item)
1006            .fontSize(16)
1007            .fontColor($r('sys.color.ohos_id_color_primary'))
1008        }, (item: ResourceStr) => JSON.stringify(item))
1009      }
1010      .width('100%')
1011      .alignItems(HorizontalAlign.Start)
1012    }
1013    .width('100%')
1014    .padding({
1015      left: 16,
1016      right: 16
1017    })
1018  }
1019}
1020
1021class BasicDataSource<T> implements IDataSource {
1022  private listeners: DataChangeListener[] = [];
1023  private originDataArray: T[] = [];
1024
1025  public totalCount(): number {
1026    return 0;
1027  }
1028
1029  public getData(index: number): T {
1030    return this.originDataArray[index];
1031  }
1032
1033  registerDataChangeListener(listener: DataChangeListener): void {
1034    if (this.listeners.indexOf(listener) < 0) {
1035      this.listeners.push(listener);
1036    }
1037  }
1038
1039  unregisterDataChangeListener(listener: DataChangeListener): void {
1040    const pos = this.listeners.indexOf(listener);
1041    if (pos >= 0) {
1042      this.listeners.splice(pos, 1);
1043    }
1044  }
1045
1046  notifyDataAdd(index: number): void {
1047    this.listeners.forEach(listener => {
1048      listener.onDataAdd(index);
1049    });
1050  }
1051}
1052
1053export class MyDataSource<T> extends BasicDataSource<T> {
1054  private dataArray: T[] = [];
1055
1056  public totalCount(): number {
1057    return this.dataArray.length;
1058  }
1059
1060  public getData(index: number): T {
1061    return this.dataArray[index];
1062  }
1063
1064  public pushData(data: T): void {
1065    this.dataArray.push(data);
1066    this.notifyDataAdd(this.dataArray.length - 1);
1067  }
1068}
1069```
1070
1071### ListItemGroup使用场景
1072
1073- 可以视作特殊List滑动场景,将ListItem需要销毁重建的子组件封装成自定义组件,并使用\@Reusable装饰器修饰,使其具备组件复用能力。
1074
1075```ts
1076@Entry
1077@Component
1078struct ListItemGroupAndReusable {
1079  data: DataSrc2 = new DataSrc2();
1080
1081  @Builder
1082  itemHead(text: string) {
1083    Text(text)
1084      .fontSize(20)
1085      .backgroundColor(0xAABBCC)
1086      .width('100%')
1087      .padding(10)
1088  }
1089
1090  aboutToAppear() {
1091    for (let i = 0; i < 10000; i++) {
1092      let data_1 = new DataSrc1();
1093      for (let j = 0; j < 12; j++) {
1094        data_1.Data.push(`测试条目数据: ${i} - ${j}`);
1095      }
1096      this.data.Data.push(data_1);
1097    }
1098  }
1099
1100  build() {
1101    Stack() {
1102      List() {
1103        LazyForEach(this.data, (item: DataSrc1, index: number) => {
1104          ListItemGroup({ header: this.itemHead(index.toString()) }) {
1105            LazyForEach(item, (ii: string, index: number) => {
1106              ListItem() {
1107                Inner({ str: ii })
1108              }
1109            })
1110          }
1111          .width('100%')
1112          .height('60vp')
1113        })
1114      }
1115    }
1116    .width('100%')
1117    .height('100%')
1118  }
1119}
1120
1121@Reusable
1122@Component
1123struct Inner {
1124  @State str: string = '';
1125
1126  aboutToReuse(param: ESObject) {
1127    this.str = param.str;
1128  }
1129
1130  build() {
1131    Text(this.str)
1132  }
1133}
1134
1135class DataSrc1 implements IDataSource {
1136  listeners: DataChangeListener[] = [];
1137  Data: string[] = [];
1138
1139  public totalCount(): number {
1140    return this.Data.length;
1141  }
1142
1143  public getData(index: number): string {
1144    return this.Data[index];
1145  }
1146
1147  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
1148  registerDataChangeListener(listener: DataChangeListener): void {
1149    if (this.listeners.indexOf(listener) < 0) {
1150      this.listeners.push(listener);
1151    }
1152  }
1153
1154  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
1155  unregisterDataChangeListener(listener: DataChangeListener): void {
1156    const pos = this.listeners.indexOf(listener);
1157    if (pos >= 0) {
1158      this.listeners.splice(pos, 1);
1159    }
1160  }
1161
1162  // 通知LazyForEach组件需要重载所有子组件
1163  notifyDataReload(): void {
1164    this.listeners.forEach(listener => {
1165      listener.onDataReloaded();
1166    });
1167  }
1168
1169  // 通知LazyForEach组件需要在index对应索引处添加子组件
1170  notifyDataAdd(index: number): void {
1171    this.listeners.forEach(listener => {
1172      listener.onDataAdd(index);
1173    });
1174  }
1175
1176  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
1177  notifyDataChange(index: number): void {
1178    this.listeners.forEach(listener => {
1179      listener.onDataChange(index);
1180    });
1181  }
1182
1183  // 通知LazyForEach组件需要在index对应索引处删除该子组件
1184  notifyDataDelete(index: number): void {
1185    this.listeners.forEach(listener => {
1186      listener.onDataDelete(index);
1187    });
1188  }
1189
1190  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
1191  notifyDataMove(from: number, to: number): void {
1192    this.listeners.forEach(listener => {
1193      listener.onDataMove(from, to);
1194    });
1195  }
1196}
1197
1198class DataSrc2 implements IDataSource {
1199  listeners: DataChangeListener[] = [];
1200  Data: DataSrc1[] = [];
1201
1202  public totalCount(): number {
1203    return this.Data.length;
1204  }
1205
1206  public getData(index: number): DataSrc1 {
1207    return this.Data[index];
1208  }
1209
1210  // 该方法为框架侧调用,为LazyForEach组件向其数据源处添加listener监听
1211  registerDataChangeListener(listener: DataChangeListener): void {
1212    if (this.listeners.indexOf(listener) < 0) {
1213      this.listeners.push(listener);
1214    }
1215  }
1216
1217  // 该方法为框架侧调用,为对应的LazyForEach组件在数据源处去除listener监听
1218  unregisterDataChangeListener(listener: DataChangeListener): void {
1219    const pos = this.listeners.indexOf(listener);
1220    if (pos >= 0) {
1221      this.listeners.splice(pos, 1);
1222    }
1223  }
1224
1225  // 通知LazyForEach组件需要重载所有子组件
1226  notifyDataReload(): void {
1227    this.listeners.forEach(listener => {
1228      listener.onDataReloaded();
1229    });
1230  }
1231
1232  // 通知LazyForEach组件需要在index对应索引处添加子组件
1233  notifyDataAdd(index: number): void {
1234    this.listeners.forEach(listener => {
1235      listener.onDataAdd(index);
1236    });
1237  }
1238
1239  // 通知LazyForEach组件在index对应索引处数据有变化,需要重建该子组件
1240  notifyDataChange(index: number): void {
1241    this.listeners.forEach(listener => {
1242      listener.onDataChange(index);
1243    });
1244  }
1245
1246  // 通知LazyForEach组件需要在index对应索引处删除该子组件
1247  notifyDataDelete(index: number): void {
1248    this.listeners.forEach(listener => {
1249      listener.onDataDelete(index);
1250    });
1251  }
1252
1253  // 通知LazyForEach组件将from索引和to索引处的子组件进行交换
1254  notifyDataMove(from: number, to: number): void {
1255    this.listeners.forEach(listener => {
1256      listener.onDataMove(from, to);
1257    });
1258  }
1259}
1260```
1261
1262
1263### 多种条目类型使用场景
1264
1265#### 标准型
1266
1267- 复用组件之间布局完全相同;
1268- 示例同列表滚动中描述;
1269
1270#### 有限变化型
1271
1272- 复用组件之间有不同,但是类型有限;
1273- 示例为复用组件显式设置两个reuseId与使用两个自定义组件进行复用;
1274
1275```ts
1276class MyDataSource implements IDataSource {
1277  private dataArray: string[] = [];
1278  private listener: DataChangeListener | undefined;
1279
1280  public totalCount(): number {
1281    return this.dataArray.length;
1282  }
1283
1284  public getData(index: number): string {
1285    return this.dataArray[index];
1286  }
1287
1288  public pushData(data: string): void {
1289    this.dataArray.push(data);
1290  }
1291
1292  public reloadListener(): void {
1293    this.listener?.onDataReloaded();
1294  }
1295
1296  public registerDataChangeListener(listener: DataChangeListener): void {
1297    this.listener = listener;
1298  }
1299
1300  public unregisterDataChangeListener(listener: DataChangeListener): void {
1301    this.listener = undefined;
1302  }
1303}
1304
1305@Entry
1306@Component
1307struct Index {
1308  private data: MyDataSource = new MyDataSource();
1309
1310  aboutToAppear() {
1311    for (let i = 0; i < 1000; i++) {
1312      this.data.pushData(i + "");
1313    }
1314  }
1315
1316  build() {
1317    Column() {
1318      List({ space: 10 }) {
1319        LazyForEach(this.data, (item: number) => {
1320          ListItem() {
1321            ReusableComponent({ item: item })
1322              .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
1323          }
1324          .backgroundColor(Color.Orange)
1325          .width('100%')
1326        }, (item: number) => item.toString())
1327      }
1328      .cachedCount(2)
1329    }
1330  }
1331}
1332
1333@Reusable
1334@Component
1335struct ReusableComponent {
1336  @State item: number = 0;
1337
1338  aboutToReuse(params: ESObject) {
1339    this.item = params.item;
1340  }
1341
1342  build() {
1343    Column() {
1344      if (this.item % 2 === 0) {
1345        Text(`Item ${this.item} ReusableComponentOne`)
1346          .fontSize(20)
1347          .margin({ left: 10 })
1348      } else {
1349        Text(`Item ${this.item} ReusableComponentTwo`)
1350          .fontSize(20)
1351          .margin({ left: 10 })
1352      }
1353    }.margin({ left: 10, right: 10 })
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  // itemBuilderOne作为复用组件的写法未展示,以下为转为Builder之后的写法
1406  @Builder
1407  itemBuilderOne(item: string) {
1408    Column() {
1409      ChildComponentA({ item: item })
1410      ChildComponentB({ item: item })
1411      ChildComponentC({ item: item })
1412    }
1413  }
1414
1415  // itemBuilderTwo转为Builder之后的写法
1416  @Builder
1417  itemBuilderTwo(item: string) {
1418    Column() {
1419      ChildComponentA({ item: item })
1420      ChildComponentC({ item: item })
1421      ChildComponentD({ item: item })
1422    }
1423  }
1424
1425  // itemBuilderThree转为Builder之后的写法
1426  @Builder
1427  itemBuilderThree(item: string) {
1428    Column() {
1429      ChildComponentA({ item: item })
1430      ChildComponentB({ item: item })
1431      ChildComponentD({ item: item })
1432    }
1433  }
1434
1435  build() {
1436    List({ space: 40 }) {
1437      LazyForEach(this.data, (item: string, index: number) => {
1438        ListItem() {
1439          if (index % 3 === 0) {
1440            this.itemBuilderOne(item)
1441          } else if (index % 5 === 0) {
1442            this.itemBuilderTwo(item)
1443          } else {
1444            this.itemBuilderThree(item)
1445          }
1446        }
1447        .backgroundColor('#cccccc')
1448        .width('100%')
1449        .onAppear(() => {
1450          console.log(`ListItem ${index} onAppear`);
1451        })
1452      }, (item: number) => item.toString())
1453    }
1454    .width('100%')
1455    .height('100%')
1456    .cachedCount(0)
1457  }
1458}
1459
1460@Reusable
1461@Component
1462struct ChildComponentA {
1463  @State item: string = '';
1464
1465  aboutToReuse(params: ESObject) {
1466    console.log(`ChildComponentA ${params.item} Reuse ${this.item}`);
1467    this.item = params.item;
1468  }
1469
1470  aboutToRecycle(): void {
1471    console.log(`ChildComponentA ${this.item} Recycle`);
1472  }
1473
1474  build() {
1475    Column() {
1476      Text(`Item ${this.item} Child Component A`)
1477        .fontSize(20)
1478        .margin({ left: 10 })
1479        .fontColor(Color.Blue)
1480      Grid() {
1481        ForEach((new Array(20)).fill(''), (item: string, index: number) => {
1482          GridItem() {
1483            // 请开发者自行在src/main/resources/base/media路径下添加app.media.startIcon图片,否则运行时会因资源缺失而报错
1484            Image($r('app.media.startIcon'))
1485              .height(20)
1486          }
1487        })
1488      }
1489      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
1490      .rowsTemplate('1fr 1fr 1fr 1fr')
1491      .columnsGap(10)
1492      .width('90%')
1493      .height(160)
1494    }
1495    .margin({ left: 10, right: 10 })
1496    .backgroundColor(0xFAEEE0)
1497  }
1498}
1499
1500@Reusable
1501@Component
1502struct ChildComponentB {
1503  @State item: string = '';
1504
1505  aboutToReuse(params: ESObject) {
1506    this.item = params.item;
1507  }
1508
1509  build() {
1510    Row() {
1511      Text(`Item ${this.item} Child Component B`)
1512        .fontSize(20)
1513        .margin({ left: 10 })
1514        .fontColor(Color.Red)
1515    }.margin({ left: 10, right: 10 })
1516  }
1517}
1518
1519@Reusable
1520@Component
1521struct ChildComponentC {
1522  @State item: string = '';
1523
1524  aboutToReuse(params: ESObject) {
1525    this.item = params.item;
1526  }
1527
1528  build() {
1529    Row() {
1530      Text(`Item ${this.item} Child Component C`)
1531        .fontSize(20)
1532        .margin({ left: 10 })
1533        .fontColor(Color.Green)
1534    }.margin({ left: 10, right: 10 })
1535  }
1536}
1537
1538@Reusable
1539@Component
1540struct ChildComponentD {
1541  @State item: string = '';
1542
1543  aboutToReuse(params: ESObject) {
1544    this.item = params.item;
1545  }
1546
1547  build() {
1548    Row() {
1549      Text(`Item ${this.item} Child Component D`)
1550        .fontSize(20)
1551        .margin({ left: 10 })
1552        .fontColor(Color.Orange)
1553    }.margin({ left: 10, right: 10 })
1554  }
1555}
1556```
1557