目录

创建自定义组件

在ArkUI中,UI显示的内容均为组件,由框架直接提供的称为系统组件,由开发者定义的称为自定义组件。进行UI界面开发时,不仅要组合使用系统组件,还需考虑代码的可复用性、业务逻辑与UI的分离,以及后续版本的演进等因素。因此,将UI和部分业务逻辑封装成自定义组件是不可或缺的能力。

自定义组件具有以下特点:

从API version 24开始,可通过在应用工程的module.json5配置文件中配置metadata标签来使能自定义组件支持跨Ability迁移。具体配置方式为:新增name为"enableCustomComponentCrossAbility",value为"true"。因为自定义组件提供的是UI能力,所以这里的Ability也特指UIAbility。具体示例参考自定义组件支持跨Ability迁移。

自定义组件的基本用法

以下示例展示了自定义组件的基本用法。

@Component
struct HelloComponent {
  @State message: string = 'Hello, World!';

  build() {
    // HelloComponent自定义组件组合系统组件Row和Text
    Row() {
      Text(this.message)
        .fontSize(20)
        .margin(10)
        .onClick(() => {
          // 状态变量message的改变驱动UI刷新,UI从'Hello, World!'刷新为'Hello, ArkUI!'
          this.message = 'Hello, ArkUI!';
        })
    }
  }
}

如果在其他文件中引用自定义组件,需要使用export关键字导出组件,并在使用的页面import该自定义组件。

可以在其他自定义组件的build()函数中多次创建HelloComponent,以实现自定义组件的重用。

@Entry
@Component
struct ParentComponent {
  build() {
    Column() {
      // 多次创建HelloComponent,实现自定义组件的重用
      Text('ArkUI message')
        .fontSize(20)
        .margin(10)
      HelloComponent({ message: 'Hello World!' })
      Divider()
      HelloComponent({ message: 'Hello ArkTS!' })
    }
    .width('100%')
  }
}

要完全理解上面的示例,需要了解自定义组件的以下概念定义,本文将在后面的小节中介绍:

自定义组件的基本结构

[h2]struct

自定义组件基于struct实现,struct + 自定义组件名 + {...}的组合构成自定义组件,不能有继承关系。对于struct的实例化,可以省略new。

自定义组件名、类名、函数名不得与系统组件名重复。

[h2]@Entry

@Entry装饰的自定义组件将作为UI页面的入口。在单个UI页面中,仅允许存在一个由@Entry装饰的自定义组件作为页面的入口。

从API version 9开始,该装饰器支持在ArkTS卡片中使用。

从API version 10开始,@Entry可以接受一个可选的LocalStorage参数或者一个可选的EntryOptions10+参数。

从API version 11开始,该装饰器支持在元服务中使用。

@Entry
@Component
struct MyComponent {
  // ...
}

EntryOptions10+

命名路由跳转选项。

名称 类型 只读 可选 说明
routeName string 否 是 表示作为命名路由页面的名字。
storage LocalStorage 否 是 页面级的UI状态存储。当未传入时,框架会创建一个新的LocalStorage实例作为默认值。
useSharedStorage12+ boolean 否 是 是否使用loadContent传入的LocalStorage实例对象。默认值false。值为true时:若loadContent传入了LocalStorage实例,则使用该LocalStorage实例对象,否则会新建一个LocalStorage实例。值为false时:不使用共享的LocalStorage实例对象。

当useSharedStorage设置为true且storage已赋值时,useSharedStorage的优先级高于storage参数,此时无论loadContent中是否传入LocalStorage实例,都不会使用传入的storage参数。

@Entry({ routeName: 'myPage' })
@Component
struct MyComponent {
  // ...
}

[h2]@Component

@Component装饰的struct为V1自定义组件,可以使用状态管理V1版本装饰器的能力。

从API version 9开始,该装饰器支持在ArkTS卡片中使用。

从API version 11开始,@Component可以接受一个ComponentOptions参数。

从API version 11开始,该装饰器支持在元服务中使用。

@Component
struct MyComponent {
  // ...
}

[h2]@ComponentV2

@ComponentV2装饰的struct为V2自定义组件,可以使用状态管理V2版本装饰器的能力。

@ComponentV2装饰器从API version 12开始支持。

从API version 12开始,该装饰器支持在元服务中使用。

从API version 23开始,该装饰器支持在ArkTS卡片中使用。

和@Component装饰器一样,@ComponentV2装饰器用于装饰自定义组件:

除非特别说明,@ComponentV2装饰的自定义组件将与@Component装饰的自定义组件保持相同的行为。

[h2]build()函数

build()函数用于定义自定义组件的声明式UI描述,自定义组件必须定义build()函数。

@Component
struct MyComponent {
  build() {
    // ...
  }
}

[h2]@Reusable

@Reusable装饰V1自定义组件,使得该自定义组件具有被复用的能力。详细请参考:@Reusable装饰器:组件复用。

@Reusable
@Component
struct MyComponent {
  // ...
}

[h2]@ReusableV2

@ReusableV2装饰V2自定义组件,使得该自定义组件具有被复用的能力。详细请参考:@ReusableV2装饰器:V2组件复用。

@ReusableV2
@ComponentV2
struct MyComponent {
  // ...
}

成员函数/变量

自定义组件除了必须要实现build()函数外,还可以实现其他成员函数,成员函数具有以下约束:

自定义组件可以包含成员变量,成员变量具有以下约束:

自定义组件的参数规定

自定义组件的成员变量根据装饰器不同,初始化规则不同,各装饰器规则如下表所示。

@Component成员变量初始化规则

变量类型 本地初始化 从父组件传入
普通变量 必选 可选,传入非undefined值时使用传入值,否则使用本地默认值。
@State 必选 可选,传入非undefined值时使用传入值,否则使用本地默认值。
@Prop 可选 可选,无本地默认值时必选,传入非undefined值时使用传入值,否则使用本地默认值。
@Link 不支持 必选,需传入状态变量。
@ObjectLink 不支持 必选,需传入@Observed装饰的class实例(API version 19起可传入复杂类型)。
@Provide 必选 可选,传入非undefined值时使用传入值,否则使用本地默认值。
@Consume 不支持(API version 20起可选) 不支持,通过别名/变量名匹配@Provide初始化。
@StorageProp 必选 不支持,通过AppStorage对应key初始化。
@StorageLink 必选 不支持,通过AppStorage对应key初始化。
@LocalStorageProp 必选 不支持,通过LocalStorage对应key初始化。
@LocalStorageLink 必选 不支持,通过LocalStorage对应key初始化。

@ComponentV2成员变量初始化规则

变量类型 本地初始化 从父组件传入
普通变量 必选 不支持。
@Local 必选 不支持。
@Param 可选 可选,无本地默认值时必选,有传入值时使用传入值,否则使用本地默认值。
@Event 可选 可选,无本地默认值且未从父组件传入时,自动生成空函数作为默认回调。
@Provider 必选 不支持。
@Consumer 必选 不支持,通过别名/变量名匹配@Provider初始化。

下面以普通变量为例,展示如何在build方法中初始化自定义组件的参数。其余装饰器的使用示例,可参考各文档。

@Component
struct MyComponent {
  countDownFrom: number = 0;
  color: Color = Color.Blue;

  build() {
    Column() {
      Text(`${this.countDownFrom}`)
        .fontSize(20)
        .margin(10)
        .backgroundColor(this.color)
    }
    .width('100%')
  }
}

@Entry
@Component
struct ParentComponent {
  private someColor: Color = Color.Pink;

  build() {
    Column() {
      // 创建MyComponent实例,并将创建MyComponent成员变量countDownFrom初始化为10,将成员变量color初始化为this.someColor
      MyComponent({ countDownFrom: 10, color: this.someColor })
    }
    .width('100%')
  }
}

以下示例代码将父组件中的函数传递给子组件,并在子组件中调用。

@Entry
@Component
struct Parent {
  @State cnt: number = 0;
  submit: () => void = () => {
    this.cnt++;
  };

  build() {
    Column() {
      Text(`${this.cnt}`)
        .fontSize(20)
        .margin(10)
      // 父组件中的函数传递给子组件
      Son({ submitArrow: this.submit })
    }
    .width('100%')
  }
}

@Component
struct Son {
  submitArrow?: () => void;

  build() {
    Row() {
      Button('add')
        .width(300)
        .margin(10)
        .onClick(() => {
          if (this.submitArrow) {
            this.submitArrow()
          }
        })
    }
    .height('100%')
  }
}

build()函数实现规则

所有在build()函数中声明的语句统称为UI描述,UI描述需要遵循以下规则:

自定义组件通用样式

自定义组件通过“.”链式调用设置通用样式。

@Component
struct ChildComponent {
  build() {
    Button(`Hello World`)
      .width('90%')
      .margin(10)
  }
}

@Entry
@Component
struct MyComponent {
  build() {
    Row() {
      // 属性设置给ChildComponent而不是ChildComponent中的Button
      ChildComponent()
        .width(300)
        .height(300)
        .backgroundColor(Color.Pink)
    }
    .height('100%')
  }
}

ArkUI给自定义组件设置样式时,相当于给ChildComponent套了一个不可见的容器组件,这些样式是设置在容器组件上,而非直接设置给ChildComponent的Button组件。渲染结果显示,背景颜色粉红色并没有直接设置到Button上,而是设置在Button所在的不可见容器组件上。

自定义组件支持跨Ability迁移

API version 24前,自定义组件不支持跨Ability迁移,自定义组件实例在跨Ability后,改变自定义组件的状态变量将无法触发UI组件刷新。需要注意,在系统升级API version 24之前,即使在module.json5配置了"enableCustomComponentCrossAbility"为"true",该能力也不会生效。

API version 24开始,可在应用工程的module.json5配置文件中配置metadata标签来使能自定义组件支持跨Ability迁移。具体配置方式如下。

"metadata": [
  {
    "name": "enableCustomComponentCrossAbility",
    "value": "true"
  }
]

需要注意:

  1. 不建议在原Ability的onBackground阶段异步修改迁移组件中的状态变量,此时状态变量可以被赋值,但无法触发关联组件的刷新。
  2. 仅支持组件树上的自定义组件迁移。对于未挂载在组件树上的自定义组件将不支持迁移。例如使用OH_ArkUI_GetNodeHandleFromNapiValue获取ArkUI_NodeHandle场景中,如果OH_ArkUI_GetNodeHandleFromNapiValue接收的参数为ComponentContent,获取到的ArkUI_NodeHandle为ComponentContent下子树的第一个FrameNode节点,中间跳过的自定义组件将不会在组件树上,不支持迁移。
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class EntryAbility extends UIAbility {
  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/Index', (err) => {
      if (err.code) {
        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
    });
  }

  onBackground(): void {
    // 不建议在onBackground阶段异步修改迁移组件中的状态变量
    hilog.info(DOMAIN, 'testTag', '%{public}s', 'Ability onBackground');
  }
}

在下面的示例中:

  1. 点击Button('add node to tree'),创建BuilderNode节点挂载到NodeContainer下。
  2. 点击Button('remove node from tree'),将BuilderNode节点从NodeContainer上移除。
  3. 点击Button('start new ability'),拉起ExtraAbility。
  4. 点击ExtraIndex内的Button('add node to tree'),将BuilderNode节点重新挂载到ExtraIndex内的NodeContainer下。
    • 自定义组件ComponentUnderBuilderNode在被挂载到新的Ability下时,会通知切换Ability的自定义组件更新其所属的Ability实例ID。
    • 点击自定义组件ComponentUnderBuilderNode内Button('change message'),改变状态变量message的值,触发@Watch('messageUpdate') 回调和UI刷新。

下面的示例包含了创建新的Ability流程,具体示例可参考startAbility。

import { MyNodeController } from './MyNodeController';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { common, Want } from '@kit.AbilityKit';
import { BusinessError } from '@kit.BasicServicesKit';

const DOMAIN = 0x0000;

@Entry
@Component
struct Index {
  private nodeController: MyNodeController = new MyNodeController();

  startNewAbility() {
    const want: Want = {
      bundleName: 'com.example.enablecustomcomponentcrossability',
      abilityName: 'ExtraAbility'
    };

    try {
      const context = this.getUIContext()?.getHostContext() as common.UIAbilityContext;
      context.startAbility(want, (err: BusinessError) => {
        if (err.code) {
          hilog.error(DOMAIN, 'testTag', `startAbility failed, code is ${err.code}, message is ${err.message}`);
          return;
        }
        hilog.info(DOMAIN, 'testTag', 'startAbility succeed');
      });
    } catch (err) {
      hilog.error(DOMAIN, 'testTag',
        `startAbility failed, code is ${(err as BusinessError).code}, message is ${(err as BusinessError).message}`);
    }
  }

  build() {
    Column({ space: 10 }) {
      Text('Index')
      // 创建globalBuilderNode,并将globalBuilderNode下的节点挂在NodeContainer的占位节点下
      Button('add node to tree').width(200).onClick(() => {
        this.nodeController.addBuilderNode();
      })
      // 从NodeContainer的占位节点下移除globalBuilderNode下的节点
      Button('remove node from tree').width(200).onClick(() => {
        this.nodeController.removeBuilderNode();
      })
      // 拉起新的Ability
      Button('start new ability').width(200).onClick(() => {
        this.startNewAbility();
      })
      NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
    }
    .width('100%')
    .height('100%')
  }
}
import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI';
import { hilog } from '@kit.PerformanceAnalysisKit';

const DOMAIN = 0x0000;

let globalBuilderNode: BuilderNode<[]> | undefined = undefined;

export class MyNodeController extends NodeController {
  private rootNode: FrameNode | null = null;
  private uiContext: UIContext | null = null;

  makeNode(uiContext: UIContext): FrameNode | null {
    this.rootNode = new FrameNode(uiContext);
    this.uiContext = uiContext;
    return this.rootNode;
  }

  addBuilderNode(): void {
    if (!globalBuilderNode && this.uiContext) {
      globalBuilderNode = new BuilderNode(this.uiContext);
      globalBuilderNode.build(wrapBuilder<[]>(buildComponent), undefined);
    }
    if (this.rootNode && globalBuilderNode) {
      this.rootNode.appendChild(globalBuilderNode.getFrameNode());
    }
  }

  removeBuilderNode(): void {
    if (this.rootNode && globalBuilderNode) {
      this.rootNode.removeChild(globalBuilderNode.getFrameNode());
    }
  }

  disposeNode(): void {
    if (this.rootNode && globalBuilderNode) {
      globalBuilderNode.dispose();
      globalBuilderNode = undefined;
    }
  }
}

@Builder
function buildComponent() {
  Column() {
    ComponentUnderBuilderNode()
  }
}

@Component
struct ComponentUnderBuilderNode {
  @State @Watch('messageUpdate') message: string = 'hello';

  messageUpdate() {
    hilog.info(DOMAIN, 'testTag', `ComponentUnderBuilderNode message change ${this.message}`);
  }

  build() {
    Column() {
      Text(`message: ${this.message}`)
      // 改变message的值,触发@Watch('messageUpdate')回调和Text组件的刷新
      Button('change message').onClick(() => {
        this.message += ' world';
      })
    }
  }
}
import { UIAbility } from '@kit.AbilityKit';
import { hilog } from '@kit.PerformanceAnalysisKit';
import { window } from '@kit.ArkUI';

const DOMAIN = 0x0000;

export default class ExtraAbility extends UIAbility {

  onWindowStageCreate(windowStage: window.WindowStage): void {
    windowStage.loadContent('pages/ExtraIndex', (err) => {
      if (err.code) {
        // ExtraIndex加载失败,输出报错信息
        hilog.error(DOMAIN, 'testTag', 'Failed to load the content. Cause: %{public}s', JSON.stringify(err));
        return;
      }
      hilog.info(DOMAIN, 'testTag', 'Succeeded in loading the content.');
    });
  }
}
import { MyNodeController } from './MyNodeController';

@Entry
@Component
struct ExtraIndex {
  private nodeController: MyNodeController = new MyNodeController();

  build() {
    Column({ space: 10 }) {
      Text('ExtraIndex')
      // 将globalBuilderNode下的节点挂在NodeContainer的占位节点下
      Button('add node to tree').width(200).onClick(() => {
        this.nodeController.addBuilderNode();
      })
      // 从NodeContainer的占位节点下移除globalBuilderNode下的节点
      Button('remove node from tree').width(200).onClick(() => {
        this.nodeController.removeBuilderNode();
      })
      // 销毁globalBuilderNode下的节点
      Button('dispose node').width(200).onClick(() => {
        this.nodeController.disposeNode();
      })
      NodeContainer(this.nodeController).backgroundColor('#FFEEF0')
    }
    .width('100%')
    .height('100%')
  }
}

限制条件

[h2]V1自定义组件不支持静态代码块

静态代码块用于初始化静态属性。

[h2]@Component与@ComponentV2混用

在将@Component装饰的自定义组件与@ComponentV2装饰的自定义组件混合使用时,可参考状态管理V1和V2混用场景。

[h2]@Reusable或@ReusableV2混用

当将@Reusable或@ReusableV2装饰的复用组件与其他自定义组件混合使用时,可参考使用限制。