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独有属性
  }
}

当前方案的缺点如下:

  1. 使用方式和系统组件不一致:系统组件通过链式调用的方式设置组件属性,该方案自定义组件需要以“参数列表”形式设置组件属性。
  2. 自定义组件入参过大:若需要使用系统组件的全量属性方法,则需在封装的自定义组件中以入参的形式穷举接收每个属性值。在使用自定义组件时,也需将全量的属性值以参数形式传入。
  3. 不利于后期维护:当自定义组件中的系统组件属性发生变更时,自定义组件也需要同步适配。

实现方案

为解决上述方案缺点,ArkTS为每个系统组件提供了attributeModifier属性方法。该方法将组件属性设置分离到系统提供的AttributeModifier接口实现类实例中,通过自定义Class类实现AttributeModifier接口对系统组件属性进行扩展。通过AttributeModifier实现公用组件有如下两种方案:

方案一:提供方对外提供封装好的自定义组件。

以封装系统组件Button为例,该方案实现步骤如下:

  1. 提供方在公共组件库中创建公用的自定义组件,该组件支持外部传入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)
  }
}
  1. 使用方自定义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接口的实现类。

  1. 提供方创建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;
  }
}
  1. 使用方创建提供方的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 图片和文本组合组件效果

针对固定组合的组件封装采用方案一,实现上述效果的示例代码如下:

  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)
    }
  }
}
  1. 使用方分别实现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)
  }
}
  1. 使用方创建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弹窗效果

  1. 提供方创建自定义弹窗组件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)
  }
}
  1. 提供方创建自定义组件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() {
  }
}
  1. 使用方导入自定义组件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函数的返回值。组件工厂场景的实现主要包含以下步骤:

  1. 在组件工厂实现方,将需要工厂化的组件通过全局@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' })
  }
}
  1. 在组件工厂实现方,将封装好的全局@Builder方法使用wrapBuilder函数包裹,并将返回值作为组件工厂Map的value值存入。全部组件存入后,将组件工厂导出供外部使用。
// 定义组件工厂Map
let factoryMap: Map<string, object> = new Map();

// 将需要工厂化的组件存入到组件工厂中
factoryMap.set('Radio', wrapBuilder(MyRadio));
factoryMap.set('Checkbox', wrapBuilder(MyCheckbox));

// 导出组件工厂
export { factoryMap }
  1. 在使用方,引入组件工厂并通过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<[]>;
  1. 在使用方的组件build方法中,通过调用WrappedBuilder对象的builder方法获取具体组件。
@Component
struct Index {
  build() {
    Column() {
      // myRadio和myCheckbox是从组件工厂中获取的WrappedBuilder对象
      myRadio.builder();
      myCheckbox.builder();
    }
  }
}

说明

使用wrapBuilder方法有以下限制:

  1. wrapBuilder方法只支持传入全局@Builder方法。
  2. wrapBuilder方法返回的WrappedBuilder对象的builder属性方法只能在struct内部使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值