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