来自云龙湖轮廓分明的月亮 发表于 2024-8-29 08:46:51

HarmonyOS开辟知识:ArkUI公用组件封装场景指南

配景

在应用开辟中,通常必要对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 图片和文本组合组件效果

https://img-blog.csdnimg.cn/img_convert/867d0e10245f28534706fbbf836d5818.png
针对固定组合的组件封装采用方案一,实现上述效果的示例代码如下:

[*]提供方封装自定义组件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弹窗场景
https://img-blog.csdnimg.cn/img_convert/4ef4ca6cce5af95685242cb318eae1b3.png
方案先容

系统提供@CustomDialog装饰器用于自定义弹窗的实现。以使用方点击按钮后展示提供方的Dialog弹窗场景为例,若需实现下图效果,Dialog弹窗组件的封装和使用步骤如下:
图3 使用Dialog弹窗效果
https://img-blog.csdnimg.cn/img_convert/c1f6ba5a714818605f00a0e9e6ec6e9d.gif

[*]提供方创建自定义弹窗组件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 组件工厂场景
https://img-blog.csdnimg.cn/img_convert/e95e6ee844f80bf547091b3107f6c9e7.png
对于该场景,思量使用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内部使用。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
页: [1]
查看完整版本: HarmonyOS开辟知识:ArkUI公用组件封装场景指南