目录

@Reusable装饰器:V1组件复用

@Reusable装饰的自定义组件支持组件复用。当自定义组件从组件树上移除时,会被存入缓存池,后续在创建相同类型的组件节点时,将优先复用缓存池中的组件对象,从而避免重复创建和销毁,提升性能。

API version 10开始支持@Reusable,支持在ArkTS中使用。

概述

@Reusable用于装饰自定义组件,表示该自定义组件具有被复用的能力。

在开发复杂界面时,UI渲染效率是一个需要考虑的问题。例如在长列表快速滑动时,大量列表项的创建和销毁可能导致界面卡顿。组件复用是一种优化UI性能的重要方法。通过复用先前创建并且已经下树的组件对象,降低组件创建和销毁的频率,从而减小计算开销,提升UI渲染效率。

  • @Reusable装饰的自定义组件在从组件树中移除时,自定义组件(包含视图节点、组件实例和状态上下文)将被放入其父自定义组件的缓存池中。后续创建新自定义组件节点时,将优先复用缓存池中的节点,从而节约组件重新创建的时间。
  • @Reusable提供了aboutToRecycle和aboutToReuse两个生命周期,在组件被回收时调用aboutToRecycle,在组件被复用时调用aboutToReuse。开发者可以在这两个生命周期中实现组件回收、复用相关的业务逻辑。
  • @Reusable装饰的自定义组件下有子组件时,会在回收和复用时递归调用子组件的aboutToRecycle和aboutToReuse(与子组件是否被@Reusable标记无关),直到遍历完所有子组件。
  • 组件复用前后应保持组件结构不变。针对组件结构存在差异的场景,可以使用reuseId来区分不同结构的复用组件。
  • 当@Reusable装饰的自定义组件无子组件时,不会触发回收和复用。

限制条件

[h2]仅用于自定义组件

@Reusable装饰器仅用于自定义组件@Component,不可与@Builder搭配使用。

@Reusable不支持跟@ComponentV2搭配使用,@ComponentV2组件复用推荐@ReusableV2装饰器。

import { ComponentContent } from '@kit.ArkUI';

// @Builder不能与@Reusable搭配使用。
// @Reusable
@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
  Crash();
}

@Component
export struct Crash {
  build() {
    Column() {
      Text('Crash')
        .fontSize(12)
        .lineHeight(18)
        .fontColor(Color.Blue)
        .margin({
          left: 6
        })
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  private uiContext = this.getUIContext();

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('Index')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
          });
          this.uiContext.getPromptAction().openCustomDialog(contentNode);
        })
    }
    .height('100%')
    .width('100%')
  }
}

[h2]状态变量更新限制

被@Reusable装饰的自定义组件在复用时,会递归调用该自定义组件及其所有子组件的aboutToReuse回调函数。若在子组件的aboutToReuse函数中修改了父组件的状态变量,此次修改将不会生效,请避免此类用法。若需设置父组件的状态变量,可使用setTimeout设置延迟执行,将任务移出组件复用的作用范围,使修改生效。

【反例】

在子组件的aboutToReuse中,直接修改父组件的状态变量。

class IncorrectBasicDataSource implements IDataSource {
  private listener: DataChangeListener | undefined = undefined;
  public dataArray: number[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): number {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct IncorrectIndex {
  private data: IncorrectBasicDataSource = new IncorrectBasicDataSource();

  aboutToAppear(): void {
    for (let index = 1; index < 20; index++) {
      this.data.dataArray.push(index);
    }
  }

  build() {
    List() {
      LazyForEach(this.data, (item: number, index: number) => {
        ListItem() {
          IncorrectReuseComponent({ num: item });
        }
      }, (item: number, index: number) => index.toString())
    }.cachedCount(0)
  }
}

@Reusable
@Component
struct IncorrectReuseComponent {
  @State num: number = 0;

  aboutToReuse(params: ESObject): void {
    this.num = params.num;
  }

  build() {
    Column() {
      Text('ReuseComponent num:' + this.num.toString())
      IncorrectReuseComponentChild({ num: this.num })
      Button('plus')
        .onClick(() => {
          // 每次点击增加10
          this.num += 10;
        })
    }
    .height(200)
  }
}

@Component
struct IncorrectReuseComponentChild {
  @Link num: number;

  aboutToReuse(params: ESObject): void {
    this.num = -1 * params.num;
  }

  build() {
    Text('ReuseComponentChild num:' + this.num.toString())
  }
}

【正例】

在子组件的aboutToReuse中,使用setTimeout,将修改移出组件复用的作用范围。

class BasicDataSource implements IDataSource {
  private listener: DataChangeListener | undefined = undefined;
  public dataArray: number[] = [];

  totalCount(): number {
    return this.dataArray.length;
  }

  getData(index: number): number {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct Index {
  private data: BasicDataSource = new BasicDataSource();

  aboutToAppear(): void {
    for (let index = 1; index <= 20; index++) { // 循环20次
      this.data.dataArray.push(index);
    }
  }

  build() {
    List() {
      LazyForEach(this.data, (item: number, index: number) => {
        ListItem() {
          ReuseComponent({ num: item })
        }
      }, (item: number, index: number) => index.toString())
    }.cachedCount(0)
  }
}

@Reusable
@Component
struct ReuseComponent {
  @State num: number = 0;

  aboutToReuse(params: ESObject): void {
    this.num = params.num;
  }

  build() {
    Column() {
      Text('ReuseComponent num:' + this.num.toString())
      ReuseComponentChild({ num: this.num })
      Button('plus')
        .onClick(() => {
          this.num += 10; // 每次点击增加10
        })
    }
    .height(200)
  }
}

@Component
struct ReuseComponentChild {
  @Link num: number;

  aboutToReuse(params: ESObject): void {
    setTimeout(() => {
      this.num = -1 * params.num;
    }, 1)
  }

  build() {
    Text('ReuseComponentChild num:' + this.num.toString());
  }
}

[h2]组件结构需一致

被@Reusable装饰的自定义组件在复用前后,应保持组件的结构不变。否则,会在复用过程中创建或销毁子组件,降低复用效率和性能,甚至造成应用行为异常。

对于复用过程中创建的子组件,框架会在其创建后依次调用aboutToReuse方法和aboutToAppear方法。在调用aboutToReuse方法时,由于其aboutToAppear方法还未执行,且内部子组件还未创建,因此aboutToReuse方法中依赖aboutToAppear方法执行结果,或依赖内部子组件状态的相关操作会引起预期外的行为。在调用aboutToReuse方法后,框架会再调用aboutToAppear方法并初始化组件。

针对组件结构存在差异的场景,开发者需要通过设定不同的reuseId来进行区分,具体方式请参考多种条目类型使用场景。

【反例】

组件结构存在差异,但未通过reuseId进行区分。

以下示例中,先点击“show/hide branch A”按钮,组件被回收,再点击“show/hide branch B”按钮,组件被复用。子组件ReusableChildB在复用过程中被创建,aboutToReuse方法和aboutToAppear方法被依次调用。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

@Entry
@Component
struct Index {
  @State showBranchA: boolean = true;
  @State showBranchB: boolean = false;

  build() {
    Column({ space: 5 }) {
      Button('show/hide branch A')
        .onClick(() => {
          this.showBranchA = !this.showBranchA;
        })
      if (this.showBranchA) {
        // 组件结构存在差异,需要通过reuseId进行区分
        ReusableComponent({ flag: true })
      }
      Button('show/hide branch B')
        .onClick(() => {
          this.showBranchB = !this.showBranchB;
        })
      if (this.showBranchB) {
        // 组件结构存在差异,需要通过reuseId进行区分
        ReusableComponent({ flag: false })
      }
    }
  }
}

@Reusable
@Component
struct ReusableComponent {
  @Require @Prop flag: boolean = true;

  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
  }

  aboutToReuse(params: ESObject) {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
    this.flag = params.flag;
  }

  build() {
    Column({ space: 5 }) {
      Text('ReusableComponent')
      if (this.flag) {
        ReusableChildA()
      } else {
        ReusableChildB()
      }
    }.border({ width: 1 })
  }
}

@Component
struct ReusableChildA {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
  }

  build() {
    Text('ReusableChildA')
      .border({ width: 1 })
  }
}

@Component
struct ReusableChildB {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
  }

  build() {
    Text('ReusableChildB')
      .border({ width: 1 })
  }
}

【正例】

组件结构存在差异,通过reuseId进行区分。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

@Entry
@Component
struct Index {
  @State showBranchA: boolean = true;
  @State showBranchB: boolean = false;

  build() {
    Column({ space: 5 }) {
      Button('show/hide branch A')
        .onClick(() => {
          this.showBranchA = !this.showBranchA;
        })
      if (this.showBranchA) {
        ReusableComponent({ flag: true })
          .reuseId('ReuseA') // 通过reuseId区分不同结构的复用组件
      }
      Button('show/hide branch B')
        .onClick(() => {
          this.showBranchB = !this.showBranchB;
        })
      if (this.showBranchB) {
        ReusableComponent({ flag: false })
          .reuseId('ReuseB') // 通过reuseId区分不同结构的复用组件
      }
    }
  }
}

@Reusable
@Component
struct ReusableComponent {
  @Require @Prop flag: boolean = true;

  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToAppear');
  }

  aboutToReuse(params: ESObject) {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableComponent aboutToReuse');
    this.flag = params.flag;
  }

  build() {
    Column({ space: 5 }) {
      Text('ReusableComponent')
      if (this.flag) {
        ReusableChildA()
      } else {
        ReusableChildB()
      }
    }.border({ width: 1 })
  }
}

@Component
struct ReusableChildA {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildA aboutToReuse');
  }

  build() {
    Text('ReusableChildA')
      .border({ width: 1 })
  }
}

@Component
struct ReusableChildB {
  aboutToAppear() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToAppear');
  }

  aboutToReuse() {
    hilog.info(DOMAIN, TAG, BUNDLE + 'ReusableChildB aboutToReuse');
  }

  build() {
    Text('ReusableChildB')
      .border({ width: 1 })
  }
}

[h2]不支持ComponentContent

ComponentContent不支持传入@Reusable装饰器装饰的自定义组件。

import { ComponentContent } from '@kit.ArkUI';

@Builder
function buildCreativeLoadingDialog(closedClick: () => void) {
  Crash();
}

// 如果注释掉就可以正常弹出弹窗,如果加上@Reusable就直接crash。
@Reusable
@Component
export struct Crash {
  build() {
    Column() {
      Text('Crash')
        .fontSize(12)
        .lineHeight(18)
        .fontColor(Color.Blue)
        .margin({
          left: 6
        })
    }.width('100%')
    .height('100%')
    .justifyContent(FlexAlign.Center)
  }
}

@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  private uiContext = this.getUIContext();

  build() {
    RelativeContainer() {
      Text(this.message)
        .id('Index')
        .fontSize(50)
        .fontWeight(FontWeight.Bold)
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
        .onClick(() => {
          // ComponentContent底层是BuilderNode,BuilderNode不支持传入@Reusable装饰器装饰的自定义组件。
          let contentNode = new ComponentContent(this.uiContext, wrapBuilder(buildCreativeLoadingDialog), () => {
          });
          this.uiContext.getPromptAction().openCustomDialog(contentNode);
        })
    }
    .height('100%')
    .width('100%')
  }
}

[h2]不建议嵌套使用

@Reusable装饰器不建议嵌套使用,会增加内存,降低复用效率,加大维护难度。嵌套使用会导致额外缓存池的生成,各缓存池拥有相同树状结构,复用效率低下。此外,嵌套使用会使生命周期管理复杂,资源和变量共享困难。

使用场景

[h2]动态布局更新

重复创建与移除视图可能引起频繁的布局计算,从而影响帧率。采用组件复用可以避免不必要的视图创建与布局计算,提升性能。

以下示例中,将Child自定义组件标记为复用组件,通过Button点击更新Child,触发复用。

// xxx.ets
export class Message {
  public value: string | undefined;

  constructor(value: string) {
    this.value = value;
  }
}

@Entry
@Component
struct Index {
  @State switch: boolean = true;

  build() {
    Column() {
      Button('Hello')
        .fontSize(30)
        .fontWeight(FontWeight.Bold)
        .onClick(() => {
          this.switch = !this.switch;
        })
      if (this.switch) {
        // 如果只有一个复用的组件,可以不用设置reuseId。
        Child({ message: new Message('Child') })
          .reuseId('Child');
      }
    }
    .height('100%')
    .width('100%')
  }
}

@Reusable
@Component
struct Child {
  @State message: Message = new Message('AboutToReuse');

  aboutToReuse(params: Record<string, ESObject>) {
    this.message = params.message as Message;
  }

  build() {
    Column() {
      Text(this.message.value)
        .fontSize(30)
    }
    .borderWidth(1)
    .height(100)
  }
}

[h2]列表滚动配合LazyForEach使用

当应用展示大量数据的列表并进行滚动操作时,频繁创建和销毁列表项视图可能导致卡顿和性能问题。使用列表组件的组件复用机制可以重用已创建的列表项视图,提高滚动流畅度。

以下示例代码将CardView自定义组件标记为复用组件,List上下滑动,触发CardView复用。

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];
  private listener: DataChangeListener | undefined;

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
  }

  public reloadListener(): void {
    this.listener?.onDataReloaded();
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct ReuseDemo {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 1; i <= 1000; i++) { // 循环1000次
      this.data.pushData(i + '');
    }
  }

  build() {
    Column() {
      List() {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            CardView({ item: item });
          }
        }, (item: string) => item)
      }
    }
  }
}

// 复用组件
@Reusable
@Component
export struct CardView {
  // 被@State修饰的变量item才能更新,未被@State修饰的变量不会更新。
  @State item: string = '';

  aboutToReuse(params: Record<string, Object>): void {
    this.item = params.item as string;
  }

  build() {
    Column() {
      Text(this.item)
        .fontSize(30)
    }
    .borderWidth(1)
    .height(100)
  }
}

[h2]列表滚动-if使用场景

以下示例代码将OneMoment自定义组件标记为复用组件。当List上下滑动时,会触发OneMoment的复用。设置reuseId可为复用组件分配复用组,相同reuseId的组件将在同一复用组中复用。单个复用组件无需设置reuseId。使用reuseId标识复用组件,可避免重复执行if语句的删除和重新创建逻辑,提高复用效率和性能。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

@Entry
@Component
struct Index {
  private dataSource = new MyDataSource<FriendMoment>();

  aboutToAppear(): void {
    for (let i = 0; i < 20; i++) { // 循环20次
      let title = i + 1 + 'test_if';
      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而Image空白。
      this.dataSource.pushData(new FriendMoment(i.toString(), title, 'app.media.app_icon'));
    }

    for (let i = 0; i < 50; i++) { // 循环50次
      let title = i + 1 + 'test_if';
      this.dataSource.pushData(new FriendMoment(i.toString(), title, ''));
    }
  }

  build() {
    Column() {
      // TopBar()
      List({ space: 3 }) {
        LazyForEach(this.dataSource, (moment: FriendMoment) => {
          ListItem() {
            // 使用reuseId进行组件复用的控制。
            OneMoment({ moment: moment })
              .reuseId((moment.image !== '') ? 'withImage' : 'noImage');
          }
        }, (moment: FriendMoment) => moment.id)
      }
      .cachedCount(0)
    }
  }
}

class FriendMoment {
  public id: string = '';
  public text: string = '';
  public title: string = '';
  public image: string = '';
  public answers: Array<ResourceStr> = [];

  constructor(id: string, title: string, image: string) {
    this.text = id;
    this.title = title;
    this.image = image;
  }
}

@Reusable
@Component
export struct OneMoment {
  @Prop moment: FriendMoment;

  // 复用id相同的组件才能触发复用。
  aboutToReuse(params: ESObject): void {
    hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====OneMoment==复用了==' + this.moment.text);
  }

  build() {
    Column() {
      Text(this.moment.text)
      // if分支判断。
      if (this.moment.image !== '') {
        Flex({ wrap: FlexWrap.Wrap }) {
          Image($r(this.moment.image)).height(50).width(50);
          Image($r(this.moment.image)).height(50).width(50);
          Image($r(this.moment.image)).height(50).width(50);
          Image($r(this.moment.image)).height(50).width(50);
        }
      }
    }
  }
}

class BasicDataSource<T> implements IDataSource {
  private listeners: DataChangeListener[] = [];
  private originDataArray: T[] = [];

  public totalCount(): number {
    return 0;
  }

  public getData(index: number): T {
    return this.originDataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
    if (this.listeners.indexOf(listener) < 0) {
      this.listeners.push(listener);
    }
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
    const pos = this.listeners.indexOf(listener);
    if (pos >= 0) {
      this.listeners.splice(pos, 1);
    }
  }

  notifyDataAdd(index: number): void {
    this.listeners.forEach(listener => {
      listener.onDataAdd(index);
    });
  }
}

export class MyDataSource<T> extends BasicDataSource<T> {
  private dataArray: T[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): T {
    return this.dataArray[index];
  }

  public pushData(data: T): void {
    this.dataArray.push(data);
    this.notifyDataAdd(this.dataArray.length - 1);
  }
}

[h2]列表滚动-ForEach使用场景

使用ForEach创建可复用的自定义组件,由于ForEach渲染控制语法的全展开属性,导致复用组件无法复用。示例中点击update,数据刷新成功,但滑动列表时,ListItemView无法复用。点击clear,再次点击update,ListItemView复用成功,因为一帧内重复创建多个已被销毁的自定义组件。

// xxx.ets
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
struct Index {
  private data: MyDataSource = new MyDataSource();
  private data02: MyDataSource = new MyDataSource();
  @State isShow: boolean = true;
  @State dataSource: ListItemObject[] = [];

  aboutToAppear() {
    for (let i = 0; i < 100; i++) { // 循环100次
      this.data.pushData(i.toString());
    }

    for (let i = 30; i < 80; i++) { // 循环50次
      this.data02.pushData(i.toString());
    }
  }

  build() {
    Column() {
      Row() {
        Button('clear').onClick(() => {
          for (let i = 1; i <= 50; i++) { // 循环50次
            this.dataSource.pop();
          }
        }).height(40)

        Button('update').onClick(() => {
          for (let i = 1; i <= 50; i++) { // 循环50次
            let obj = new ListItemObject();
            obj.id = i;
            obj.uuid = Math.random().toString();
            obj.isExpand = false;
            this.dataSource.push(obj);
          }
        }).height(40)
      }

      List({ space: 10 }) {
        ForEach(this.dataSource, (item: ListItemObject) => {
          ListItem() {
            ListItemView({
              obj: item
            })
          }
        }, (item: ListItemObject) => {
          return item.uuid.toString();
        })

      }.cachedCount(0)
      .width('100%')
      .height('100%')
    }
  }
}

@Reusable
@Component
struct ListItemView {
  @ObjectLink obj: ListItemObject;
  @State item: string = '';

  aboutToAppear(): void {
    // 点击 update,首次进入,上下滑动,由于ForEach折叠展开属性,无法复用。
    hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToAppear=====ListItemView==创建了==' + this.item);
  }

  aboutToReuse(params: ESObject) {
    this.item = params.item;
    // 点击clear,再次update,复用成功。
    // 符合一帧内重复创建多个已被销毁的自定义组件。
    hilog.info(DOMAIN, TAG, BUNDLE + '=====aboutToReuse====ListItemView==复用了==' + this.item);
  }

  build() {
    Column({ space: 10 }) {
      Text(`${this.obj.id}.标题`)
        .fontSize(16)
        .fontColor('#000000')
        .padding({
          top: 20,
          bottom: 20,
        })

      if (this.obj.isExpand) {
        Text('')
          .fontSize(14)
          .fontColor('#999999')
      }
    }
    .width('100%')
    .borderRadius(10)
    .backgroundColor(Color.White)
    .padding(15)
    .onClick(() => {
      this.obj.isExpand = !this.obj.isExpand;
    })
  }
}

@Observed
class ListItemObject {
  public uuid: string = '';
  public id: number = 0;
  public isExpand: boolean = false;
}

[h2]Grid使用场景

示例中使用@Reusable装饰器修饰GridItem中的自定义组件ReusableChildComponent,即表示其具备组件复用的能力。

使用aboutToReuse可以在Grid滑动时,从复用缓存中加入到组件树之前触发,从而更新组件状态变量,展示正确内容。

需要注意的是无需在aboutToReuse中对@Link、@StorageLink、@ObjectLink、@Consume等自动更新值的状态变量进行更新,否则可能触发不必要的组件刷新。

// MyDataSource类实现IDataSource接口。
class MyDataSource implements IDataSource {
  private dataArray: number[] = [];

  public pushData(data: number): void {
    this.dataArray.push(data);
  }

  // 数据源的数据总量。
  public totalCount(): number {
    return this.dataArray.length;
  }

  // 返回指定索引位置的数据。
  public getData(index: number): number {
    return this.dataArray[index];
  }

  registerDataChangeListener(listener: DataChangeListener): void {
  }

  unregisterDataChangeListener(listener: DataChangeListener): void {
  }
}

@Entry
@Component
struct MyComponent {
  // 数据源。
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 1; i <= 1000; i++) { // 循环1000次
      this.data.pushData(i);
    }
  }

  build() {
    Column({ space: 5 }) {
      Grid() {
        LazyForEach(this.data, (item: number) => {
          GridItem() {
            // 使用可复用自定义组件。
            ReusableChildComponent({ item: item });
          }
        }, (item: number) => item.toString())
      }
      .cachedCount(2) // 设置GridItem的缓存数量。
      .columnsTemplate('1fr 1fr 1fr')
      .columnsGap(10)
      .rowsGap(10)
      .margin(10)
      .height(500)
      .backgroundColor(0xFAEEE0)
    }
  }
}

@Reusable
@Component
struct ReusableChildComponent {
  @State item: number = 0;

  // aboutToReuse从复用缓存中加入到组件树之前调用,可在此处更新组件的状态变量以展示正确的内容。
  // aboutToReuse参数类型已不支持any,这里使用Record指定明确的数据类型。Record用于构造一个对象类型,其属性键为Keys,属性值为Type。
  aboutToReuse(params: Record<string, number>) {
    this.item = params.item;
  }

  build() {
    Column() {
      // 请开发者自行在src/main/resources/base/media路径下添加app.media.app_icon图片,否则运行时会因资源缺失而报错。
      Image($r('app.media.app_icon'))
        .objectFit(ImageFit.Fill)
        .layoutWeight(1)
      Text(`图片${this.item}`)
        .fontSize(16)
        .textAlign(TextAlign.Center)
    }
    .width('100%')
    .height(120)
    .backgroundColor(0xF9CF93)
  }
}

[h2]WaterFlow使用场景

[h2]Swiper使用场景

[h2]列表滚动-ListItemGroup使用场景

[h2]多种条目类型使用场景

标准型

复用组件的布局相同,示例参见本文列表滚动部分的描述。

有限变化型

复用组件间存在差异,但类型有限。例如,可以通过显式设置两个reuseId或使用两个自定义组件来实现复用。

class LimitedMyDataSource implements IDataSource {
  private dataArray: number[] = [];
  private listener: DataChangeListener | undefined;

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): number {
    return this.dataArray[index];
  }

  public pushData(data: number): void {
    this.dataArray.push(data);
  }

  public reloadListener(): void {
    this.listener?.onDataReloaded();
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct LimitedIndex {
  private data: LimitedMyDataSource = new LimitedMyDataSource();

  aboutToAppear() {
    for (let i = 0; i < 1000; i++) { // 循环1000次
      this.data.pushData(i);
    }
  }

  build() {
    Column() {
      List({ space: 10 }) {
        LazyForEach(this.data, (item: number) => {
          ListItem() {
            ReusableComponent({ item: item })
            // 设置两种有限变化的reuseId
              .reuseId(item % 2 === 0 ? 'ReusableComponentOne' : 'ReusableComponentTwo')
          }
          .backgroundColor(Color.Orange)
          .width('100%')
        }, (item: number) => item.toString())
      }
      .cachedCount(2)
    }
  }
}

@Reusable
@Component
struct ReusableComponent {
  @State item: number = 0;

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Column() {
      // 组件内部根据类型差异渲染
      if (this.item % 2 === 0) {
        Text(`Item ${this.item} ReusableComponentOne`)
          .fontSize(20)
          .margin({ left: 10 })
      } else {
        Text(`Item ${this.item} ReusableComponentTwo`)
          .fontSize(20)
          .margin({ left: 10 })
      }
    }.margin({ left: 10, right: 10 })
  }
}

组合型

复用组件间存在多种差异,但通常具备共同的子组件。将三种复用组件以组合型方式转换为Builder函数后,内部的共享子组件将统一置于父组件MyComponent之下。复用这些子组件时,缓存池在父组件层面实现共享,减少组件创建过程中的资源消耗。

import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = '[Sample_ReusableComponent]';
const DOMAIN = 0xF811;
const BUNDLE = 'ReusableComponent_';
const NUMBER3 = 3;
const NUMBER5 = 5;

class MyDataSource implements IDataSource {
  private dataArray: string[] = [];
  private listener: DataChangeListener | undefined;

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number): string {
    return this.dataArray[index];
  }

  public pushData(data: string): void {
    this.dataArray.push(data);
  }

  public reloadListener(): void {
    this.listener?.onDataReloaded();
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  public unregisterDataChangeListener(listener: DataChangeListener): void {
    this.listener = undefined;
  }
}

@Entry
@Component
struct MyComponent {
  private data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i < 1000; i++) { // 循环1000次
      this.data.pushData(i.toString());
    }
  }

  // itemBuilderOne作为复用组件的写法未展示,以下为转为Builder之后的写法。
  @Builder
  itemBuilderOne(item: string) {
    Column() {
      ChildComponentA({ item: item });
      ChildComponentB({ item: item });
      ChildComponentC({ item: item });
    }
  }

  // itemBuilderTwo转为Builder之后的写法。
  @Builder
  itemBuilderTwo(item: string) {
    Column() {
      ChildComponentA({ item: item });
      ChildComponentC({ item: item });
      ChildComponentD({ item: item });
    }
  }

  // itemBuilderThree转为Builder之后的写法。
  @Builder
  itemBuilderThree(item: string) {
    Column() {
      ChildComponentA({ item: item });
      ChildComponentB({ item: item });
      ChildComponentD({ item: item });
    }
  }

  build() {
    List({ space: 40 }) {
      LazyForEach(this.data, (item: string, index: number) => {
        ListItem() {
          if (index % NUMBER3 === 0) {
            this.itemBuilderOne(item);
          } else if (index % NUMBER5 === 0) {
            this.itemBuilderTwo(item);
          } else {
            this.itemBuilderThree(item);
          }
        }
        .backgroundColor('#cccccc')
        .width('100%')
        .onAppear(() => {
          hilog.info(DOMAIN, TAG, BUNDLE + `ListItem ${index} onAppear`);
        })
      }, (item: string) => item)
    }
    .width('100%')
    .height('100%')
    .cachedCount(0)
  }
}

@Reusable
@Component
struct ChildComponentA {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    hilog.info(DOMAIN, TAG, BUNDLE + `ChildComponentA ${params.item} Reuse ${this.item}`);
    this.item = params.item;
  }

  aboutToRecycle(): void {
    hilog.info(DOMAIN, TAG, BUNDLE + `ChildComponentA ${this.item} Recycle`);
  }

  build() {
    Column() {
      Text(`Item ${this.item} Child Component A`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Blue)
      Grid() {
        ForEach((new Array(20)).fill(''), (item: string, index: number) => {
          GridItem() {
            // 请开发者自行在src/main/resources/base/media路径下添加app.media.startIcon图片,否则运行时会因资源缺失而报错。
            Image($r('app.media.startIcon'))
              .height(20)
          }
        })
      }
      .columnsTemplate('1fr 1fr 1fr 1fr 1fr')
      .rowsTemplate('1fr 1fr 1fr 1fr')
      .columnsGap(10)
      .width('90%')
      .height(160)
    }
    .margin({ left: 10, right: 10 })
    .backgroundColor(0xFAEEE0)
  }
}

@Reusable
@Component
struct ChildComponentB {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Row() {
      Text(`Item ${this.item} Child Component B`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Red)
    }.margin({ left: 10, right: 10 })
  }
}

@Reusable
@Component
struct ChildComponentC {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Row() {
      Text(`Item ${this.item} Child Component C`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Green)
    }.margin({ left: 10, right: 10 })
  }
}

@Reusable
@Component
struct ChildComponentD {
  @State item: string = '';

  aboutToReuse(params: ESObject) {
    this.item = params.item;
  }

  build() {
    Row() {
      Text(`Item ${this.item} Child Component D`)
        .fontSize(20)
        .margin({ left: 10 })
        .fontColor(Color.Orange)
    }.margin({ left: 10, right: 10 })
  }
}