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