背景
在应用开发中,通常需要对ArkUI组件进行封装以便业务复用。结合目前鸿蒙化过程中的实际案例,主要包含以下三种ArkUI组件封装复用的典型业务场景:
- 公用组件封装场景:公用组件封装主要指对系统组件进行封装使用。比如公共组件库需要按照UX规范样式提供统一的系统组件样式供其他业务团队使用,如登录按钮、弹窗按钮。
- 弹窗组件封装场景:弹窗组件内部封装弹窗内容和弹窗控制器,调用方通过状态变量控制弹窗显隐。
- 组件工厂类封装场景:组件工厂类封装了全部的组件并将自身向外暴露,调用方通过传入不同的参数,从组件工厂类中获取对应的组件。
下面,本文将针对以上业务场景,具体说明各场景及其实现方案。
公用组件封装
场景描述
在应用开发过程中,不同的业务场景可能需要使用相同功能和样式的ArkUI组件。例如,登录页面登录按钮和购物页面结算按钮可能样式相同。该场景常用方法是抽取相同样式的逻辑部分,并将其封装成一个自定义组件到公共组件库中。在业务场景开发时,统一从公共组件库获取封装好的公用组件。
以Button组件为例,当多处业务场景需要使用相同样式的Button组件时,将通用逻辑封装成一个MyButton自定义组件,并在通用逻辑中定制了公共的fontSize和fontColor属性。当需要把MyButton组件以Button扩展组件的形式集成到公共组件库中,提供给外部其他团队使用时,为了使它具备Button的所有基础能力并支持以链式调用的方式使用Button组件原生的属性接口,需要在MyButton组件内穷举所有的Button属性 。自定义组件的代码如下
@Component
struct MyButton {
@Prop text: string = '';
@Prop stateEffect: boolean = true;
// ...穷举所有Button独有属性
build() {
Button(this.text)
.fontSize(12)
.fontColor('#FFFFFF')
.stateEffect(this.stateEffect)// stateEffect属性的作用是控制默认点击动画
.xxx //穷举Button其他独有属性赋值
}
}
在使用MyButton 组件时,若需修改组件显示内容text和点击动画效果stateEffect时(其他Button独有的属性用法相同),需要以参数的形式传入:
@Component
struct Index {
build() {
MyButton({ text: '点击带有动效', stateEffect: true, ... }) // 入参包含MyButton 组件中定义的全部 Button独有属性
}
}
当前方案的缺点如下:
- 使用方式和系统组件不一致:系统组件通过链式调用的方式设置组件属性,该方案自定义组件需要以“参数列表”形式设置组件属性。
- 自定义组件入参过大:若需要使用系统组件的全量属性方法,则需在封装的自定义组件中以入参的形式穷举接收每个属性值。在使用自定义组件时,也需将全量的属性值以参数形式传入。
- 不利于后期维护:当自定义组件中的系统组件属性发生变更时,自定义组件也需要同步适配。
实现方案
为解决上述方案缺点,ArkTS为每个系统组件提供了attributeModifier属性方法。该方法将组件属性设置分离到系统提供的AttributeModifier接口实现类实例中,通过自定义Class类实现AttributeModifier接口对系统组件属性进行扩展。通过AttributeModifier实现公用组件有如下两种方案:
方案一:提供方对外提供封装好的自定义组件。
以封装系统组件Button为例,该方案实现步骤如下:
- 提供方在公共组件库中创建公用的自定义组件,该组件支持外部传入attributeModifier属性。
//提供方自定义组件并导出
@Component
export struct MyButton {
@Prop text: string = '';
// 接受外部传入的AttributeModifier类实例
@Prop modifier: AttributeModifier<ButtonAttribute> | null = null;
build() {
// AttributeModifier不支持入参为CustomBuilder或Lamda表达式的属性,且不支持事件和手势。此处text只能单独通过入参传递使用。
Button(this.text)
// 将入参的AttributeModifier类实例与系统组件绑定
.attributeModifier(this.modifier)
.fontSize(20)
.width(200)
.height(50)
}
}
- 使用方自定义AttributeModifier接口实现类,并将该类实例作为参数传入提供方自定义组件。
// 使用方自定义AttributeModifier接口实现类,此处指定泛型为Button组件的属性类ButtonAttribute
class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
// 私有定义Button组件特有属性
private stateEffectValue: boolean = false;
private buttonType: ButtonType = ButtonType.Normal;
constructor() {
}
// 实现组件的普通状态下的样式方法,系统还提供了hover状态和其他状态下的样式方法
applyNormalAttribute(instance: ButtonAttribute): void {
instance.stateEffect(this.stateEffectValue);
instance.type(this.buttonType);
}
stateEffect(enable: boolean): MyButtonModifier {
this.stateEffectValue = enable
return this;
}
// 自定义属性名和系统组件属性名一致,便于链式调用时的一致性
type(buttonType: ButtonType): MyButtonModifier {
this.buttonType = buttonType;
return this;
}
}
//使用方使用提供方的公用组件MyButton
@Component
struct Index {
capsuleButtonModifier: MyButtonModifier = new MyButtonModifier().stateEffect(true).type(ButtonType.Capsule)
circleButtonModifier: MyButtonModifier = new MyButtonModifier().stateEffect(true).type(ButtonType.Circle)
build() {
Row() {
MyButton({ modifier: this.capsuleButtonModifier, text: 'Capsule Button' })
.margin({ right: 20 })
MyButton({ modifier: this.circleButtonModifier, text: 'Circle Button' })
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
}
}
方案二:提供方对外提供AttributeModifier接口的实现类。
- 提供方创建AttributeModifier接口的实现类。
// 提供方创建自定类Class类,实现系统AttributeModifier接口
export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
private buttonType: ButtonType = ButtonType.Normal;
private stateEffectValue: boolean = false;
constructor() {
}
applyNormalAttribute(instance: ButtonAttribute): void {
instance.stateEffect(this.stateEffectValue);
instance.type(this.buttonType);
// 设置默认样式
instance.width(200);
instance.height(50);
instance.fontSize(20)
}
stateEffect(enable: boolean): MyButtonModifier {
this.stateEffectValue = enable;
return this;
}
type(type: ButtonType): MyButtonModifier {
this.buttonType = type;
return this;
}
}
- 使用方创建提供方的AttributeModifier实现类实例,并作为系统组件attributeModifier属性方法的参数传入。
@Component
struct Index {
modifier = new MyButtonModifier()
.stateEffect(true)
.type(ButtonType.Capsule)
build() {
Row() {
Button('Capsule Button')
.attributeModifier(this.modifier)
}
.width('100%')
.height('100%')
}
}
对比两种方案,若需要抽取复用的公用组件为单一类型,如Button或Text,推荐使用方案二。若需要抽取复用的组件为多个系统组件的组合,如组件中包含Image组件和Text组件,则推荐使用方案一。
案例参考
若需抽取一个包含系统组件Image组件和Text组件的公用组件,效果展示如下:
图1 图片和文本组合组件效果
针对固定组合的组件封装采用方案一,实现上述效果的示例代码如下:
- 提供方封装自定义组件CustomImageText并导出。
@Component
export struct CustomImageText {
@Prop imageAttribute: AttributeModifier<ImageAttribute>;
@Prop textAttribute: AttributeModifier<TextAttribute>;
@Prop imageSrc: PixelMap | ResourceStr | DrawableDescriptor;
@Prop text: string;
build() {
Column() {
Image(this.imageSrc)
.attributeModifier(this.imageAttribute)
Text(this.text)
.attributeModifier(this.textAttribute)
}
}
}
- 使用方分别实现Image组件和Text组件的AttributeModifier接口实现类。
// Image组件的AttributeModifier接口实现类
class ImageModifier implements AttributeModifier<ImageAttribute> {
private imageWidth: Length = 0;
private imageHeight: Length = 0;
constructor(width: Length, height: Length) {
this.imageWidth = width;
this.imageHeight = height;
}
width(width: Length) {
this.imageWidth = width;
return this;
}
height(height: Length) {
this.imageHeight = height;
return this;
}
applyNormalAttribute(instance: ImageAttribute): void {
instance.width(this.imageWidth);
instance.height(this.imageHeight);
}
}
// Text组件的AttributeModifier接口实现类
class TextModifier implements AttributeModifier<TextAttribute> {
constructor() {
}
applyNormalAttribute(instance: TextAttribute): void {
instance.fontSize(16)
}
}
- 使用方创建Image组件和Text组件的AttributeModifier接口实现类实例,并作为提供方自定义组件CustomImageText的入参传入。
@Component
struct Index {
imageAttribute: ImageModifier = new ImageModifier(100, 100);
textAttribute: TextModifier = new TextModifier();
build() {
Row() {
CustomImageText({
imageAttribute: this.imageAttribute,
textAttribute: this.textAttribute,
imageSrc: $r('app.media.startIcon'),
text: 'label'
})
}
.justifyContent(FlexAlign.Center)
.backgroundColor('F1F2F3')
.width('100%')
.height('100%')
}
}
弹窗组件封装
使用Dialog弹窗组件场景
如下图所示,团队A是弹窗组件提供方,团队B是弹窗组件使用方。团队A实现了Dialog弹窗模块,并向外提供了相应接口。团队B需要在不同业务场景下展示Dialog弹窗模块中的相应的弹窗组件。例如,团队A封装了警告类型的弹窗组件Dialog并将获取组件的接口暴露给外部,团队B在业务中通过导入并使用该接口打开携带警告信息的弹窗。
图2 使用Dialog弹窗场景
方案介绍
系统提供@CustomDialog装饰器用于自定义弹窗的实现。以使用方点击按钮后展示提供方的Dialog弹窗场景为例,若需实现下图效果,Dialog弹窗组件的封装和使用步骤如下:
图3 使用Dialog弹窗效果
- 提供方创建自定义弹窗组件MyCustomDialog。
// 自定义弹窗需要使用@CustomDialog装饰器
@CustomDialog
struct MyCustomDialog {
@Link visible: boolean;
// 被@CustomDialog装饰器修饰的组件必须持有CustomDialogController类型属性参数
controller: CustomDialogController;
// 弹窗交互事件参数,点击确认和取消按钮时的回调函数
onCancel?: () => void;
onConfirm?: () => void;
build() {
Column({ space: 12 }) {
Text("Custom dialog content!")
.fontSize(20)
Row() {
Button("cancel")
.onClick(() => {
this.visible = false;
this.onCancel?.();
})
.margin({ right: 20 })
Button("confirm")
.onClick(() => {
this.visible = false;
this.onConfirm?.();
})
}
.justifyContent(FlexAlign.Center)
}
.padding(24)
}
}
- 提供方创建自定义组件Dialog,该组件是对弹窗组件MyCustomDialog的二次封装,便于外部使用。
@Component
export struct Dialog {
// 监听外部传入的visible变量,visible值发生变化时触发onChange回调函数
@Watch("onChange") @Link visible: boolean;
onCancel?: () => void;
onConfirm?: () => void;
// 通过CustomDialogController的builder参数绑定弹窗组件MyCustomDialog
private controller = new CustomDialogController({
builder: MyCustomDialog({
visible: $visible,
onCancel: this.onCancel,
onConfirm: this.onConfirm
})
})
// visible的值发生变化时触发,若visible最新值为true通过弹窗控制器打开弹窗,否则关闭弹窗
onChange() {
if (this.visible) {
this.controller.open()
} else {
this.controller.close()
}
}
build() {
}
}
- 使用方导入自定义组件Dialog并传入相应入参。
@Entry
@Component
export struct Index {
// 外部定义visible变量作为弹窗组件入参,控制弹窗显隐
@State visible: boolean = false;
@State openDialogCount: number = 0;
@State cancelDialogCount: number = 0;
build() {
Column({ space: 20 }) {
Text(`open dialog count: ${this.openDialogCount}`)
.fontSize(20)
Text(`cancel dialog count: ${this.cancelDialogCount}`)
.fontSize(20)
Button(this.visible ? 'hide' : 'show')
// 点击修改visible变量后,visible的值可以被Dialog组件监听并响应
.onClick(() => this.visible = !this.visible)
// 通过双向绑定visible变量,实现外部控制弹窗
Dialog({
visible: $visible,
onCancel: () => this.cancelDialogCount++,
onConfirm: () => this.openDialogCount++
})
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
}
}
说明
1. 被@CustomDialog修饰的struct内部必须有CustomDialogController类型的属性。
2. 二次封装的Dialog组件主要通过控制器控制弹窗,不需要任何界面,因此内部的build函数内容为空。
组件工厂类封装
场景描述
如下图所示,团队A实现了一个组件工厂类并供外部使用,该类封装了多个组件。业务团队B在不同业务需求开发场景下,希望通过组件名从组件工厂类实例获取对应的组件。例如,B团队向工厂实例中里传入组件名参数"Radio",可以获取到对应的Radio组件模板。
图4 组件工厂场景
对于该场景,考虑使用Map结构将封装的各个组件存入,使用时通过Map的key值获取相应组件。对于单个组件的传递,目前系统提供了@Builder装饰器,该装饰器使得装饰后的函数遵循自定义组件build()函数语法规则。当@Builder装饰的函数作为参数传递使用时,由于其@Builder属性不会被传递,导致传递后获取的函数在UI校验时,无法识别其UI方法特性。 针对该问题,系统提供了wrapBuilder函数,将@Builder方法传入wrapBuilder函数中可实现组件的传递使用。通过组件工厂的封装和传递,避免了在调用方的build()函数内使用多个if else展示不同组件的写法。,实现了简洁的组件封装形式。
实现方案
组件工厂以Map结构存储各种组件,其中key为组件名,value为WrappedBuilder对象。该对象支持赋值和传递,是系统提供的wrapBuilder函数的返回值。组件工厂场景的实现主要包含以下步骤:
- 在组件工厂实现方,将需要工厂化的组件通过全局@Builder方法封装。
@Builder
function MyRadio() {
Radio({ value: '1', group: 'radioGroup' })
Radio({ value: '0', group: 'radioGroup' })
}
@Builder
function MyCheckbox() {
CheckboxGroup({ group: 'checkboxGroup' }) {
Checkbox({ name: '1', group: 'checkboxGroup' })
Checkbox({ name: '0', group: 'checkboxGroup' })
}
}
- 在组件工厂实现方,将封装好的全局@Builder方法使用wrapBuilder函数包裹,并将返回值作为组件工厂Map的value值存入。全部组件存入后,将组件工厂导出供外部使用。
// 定义组件工厂Map
let factoryMap: Map<string, object> = new Map();
// 将需要工厂化的组件存入到组件工厂中
factoryMap.set('Radio', wrapBuilder(MyRadio));
factoryMap.set('Checkbox', wrapBuilder(MyCheckbox));
// 导出组件工厂
export { factoryMap }
- 在使用方,引入组件工厂并通过key值获取对应的WrappedBuilder对象。
// 导入组件工厂,路径需按照实际位置导入,此处仅做示例参考
import { factoryMap } from './ComponentFactory';
// 通过组件工厂Map的key值获取对应的WrappedBuilder对象
let myRadio: WrappedBuilder<[]> = factoryMap.get('Radio') as WrappedBuilder<[]>;
let myCheckbox: WrappedBuilder<[]> = factoryMap.get('Checkbox') as WrappedBuilder<[]>;
- 在使用方的组件build方法中,通过调用WrappedBuilder对象的builder方法获取具体组件。
@Component
struct Index {
build() {
Column() {
// myRadio和myCheckbox是从组件工厂中获取的WrappedBuilder对象
myRadio.builder();
myCheckbox.builder();
}
}
}
说明
使用wrapBuilder方法有以下限制:
- wrapBuilder方法只支持传入全局@Builder方法。
- wrapBuilder方法返回的WrappedBuilder对象的builder属性方法只能在struct内部使用。