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

打印 上一主题 下一主题

主题 790|帖子 790|积分 2380

配景

在应用开辟中,通常必要对ArkUI组件进行封装以便业务复用。结合目前鸿蒙化过程中的实际案例,紧张包含以下三种ArkUI组件封装复用的典型业务场景:


  • 公用组件封装场景:公用组件封装紧张指对系统组件进行封装使用。好比公共组件库必要按照UX规范样式提供统一的系统组件样式供其他业务团队使用,如登录按钮、弹窗按钮。
  • 弹窗组件封装场景:弹窗组件内部封装弹窗内容和弹窗控制器,调用方通过状态变量控制弹窗显隐。
  • 组件工厂类封装场景:组件工厂类封装了全部的组件并将自身向外暴露,调用方通过传入不同的参数,从组件工厂类中获取对应的组件。
下面,本文将针对以上业务场景,具体说明各场景及其实现方案。
公用组件封装

场景形貌

在应用开辟过程中,不同的业务场景可能必要使用雷同功能和样式的ArkUI组件。比方,登录页面登录按钮和购物页面结算按钮可能样式雷同。该场景常用方法是抽取雷同样式的逻辑部门,并将其封装成一个自定义组件到公共组件库中。在业务场景开辟时,统一从公共组件库获取封装好的公用组件。
以Button组件为例,当多处业务场景必要使用雷同样式的Button组件时,将通用逻辑封装成一个MyButton自定义组件,并在通用逻辑中定制了公共的fontSize和fontColor属性。当必要把MyButton组件以Button扩展组件的情势集成到公共组件库中,提供给外部其他团队使用时,为了使它具备Button的所有基础能力并支持以链式调用的方式使用Button组件原生的属性接口,必要在MyButton组件内穷举所有的Button属性 。自定义组件的代码如下
  1. @Component
  2. struct MyButton {
  3.   @Prop text: string = '';
  4.   @Prop stateEffect: boolean = true;
  5.   // ...穷举所有Button独有属性
  6.   
  7.   build() {
  8.     Button(this.text)
  9.       .fontSize(12)
  10.       .fontColor('#FFFFFF')
  11.       .stateEffect(this.stateEffect)// stateEffect属性的作用是控制默认点击动画
  12.       .xxx //穷举Button其他独有属性赋值
  13.   }
  14. }
复制代码
在使用MyButton 组件时,若需修改组件显示内容text和点击动画效果stateEffect时(其他Button独有的属性用法雷同),必要以参数的情势传入:
  1. @Component
  2. struct Index {
  3.   build() {
  4.     MyButton({ text: '点击带有动效', stateEffect: true, ... }) // 入参包含MyButton 组件中定义的全部 Button独有属性
  5.   }
  6. }
复制代码
当前方案的缺点如下:

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

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

  • 提供方在公共组件库中创建公用的自定义组件,该组件支持外部传入attributeModifier属性。
  1. //提供方自定义组件并导出
  2. @Component
  3. export struct MyButton {
  4.   @Prop text: string = '';
  5.   // 接受外部传入的AttributeModifier类实例
  6.   @Prop modifier: AttributeModifier<ButtonAttribute> | null = null;
  7.   build() {
  8.     // AttributeModifier不支持入参为CustomBuilder或Lamda表达式的属性,且不支持事件和手势。此处text只能单独通过入参传递使用。
  9.     Button(this.text)
  10.       // 将入参的AttributeModifier类实例与系统组件绑定
  11.       .attributeModifier(this.modifier)
  12.       .fontSize(20)
  13.       .width(200)
  14.       .height(50)
  15.   }
  16. }
复制代码

  • 使用方自定义AttributeModifier接口实现类,并将该类实例作为参数传入提供方自定义组件。
  1. // 使用方自定义AttributeModifier接口实现类,此处指定泛型为Button组件的属性类ButtonAttribute
  2. class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  3.   // 私有定义Button组件特有属性
  4.   private stateEffectValue: boolean = false;
  5.   private buttonType: ButtonType = ButtonType.Normal;
  6.   constructor() {
  7.   }
  8.   // 实现组件的普通状态下的样式方法,系统还提供了hover状态和其他状态下的样式方法
  9.   applyNormalAttribute(instance: ButtonAttribute): void {
  10.     instance.stateEffect(this.stateEffectValue);
  11.     instance.type(this.buttonType);
  12.   }
  13.   stateEffect(enable: boolean): MyButtonModifier {
  14.     this.stateEffectValue = enable
  15.     return this;
  16.   }
  17.   // 自定义属性名和系统组件属性名一致,便于链式调用时的一致性
  18.   type(buttonType: ButtonType): MyButtonModifier {
  19.     this.buttonType = buttonType;
  20.     return this;
  21.   }
  22. }
  23. //使用方使用提供方的公用组件MyButton
  24. @Component
  25. struct Index {
  26.   capsuleButtonModifier: MyButtonModifier = new MyButtonModifier().stateEffect(true).type(ButtonType.Capsule)
  27.   circleButtonModifier: MyButtonModifier = new MyButtonModifier().stateEffect(true).type(ButtonType.Circle)
  28.   build() {
  29.     Row() {
  30.       MyButton({ modifier: this.capsuleButtonModifier, text: 'Capsule Button' })
  31.         .margin({ right: 20 })
  32.       MyButton({ modifier: this.circleButtonModifier, text: 'Circle Button' })
  33.     }
  34.     .justifyContent(FlexAlign.Center)
  35.     .width('100%')
  36.     .height('100%')
  37.   }
  38. }
复制代码
方案二:提供方对外提供AttributeModifier接口的实现类。

  • 提供方创建AttributeModifier接口的实现类。
  1. // 提供方创建自定类Class类,实现系统AttributeModifier接口
  2. export class MyButtonModifier implements AttributeModifier<ButtonAttribute> {
  3.   private buttonType: ButtonType = ButtonType.Normal;
  4.   private stateEffectValue: boolean = false;
  5.   constructor() {
  6.   }
  7.   applyNormalAttribute(instance: ButtonAttribute): void {
  8.     instance.stateEffect(this.stateEffectValue);
  9.     instance.type(this.buttonType);
  10.     // 设置默认样式
  11.     instance.width(200);
  12.     instance.height(50);
  13.     instance.fontSize(20)
  14.   }
  15.   stateEffect(enable: boolean): MyButtonModifier {
  16.     this.stateEffectValue = enable;
  17.     return this;
  18.   }
  19.   type(type: ButtonType): MyButtonModifier {
  20.     this.buttonType = type;
  21.     return this;
  22.   }
  23. }
复制代码

  • 使用方创建提供方的AttributeModifier实现类实例,并作为系统组件attributeModifier属性方法的参数传入。
  1. @Component
  2. struct Index {
  3.   modifier = new MyButtonModifier()
  4.     .stateEffect(true)
  5.     .type(ButtonType.Capsule)
  6.   build() {
  7.     Row() {
  8.       Button('Capsule Button')
  9.         .attributeModifier(this.modifier)
  10.     }
  11.     .width('100%')
  12.     .height('100%')
  13.   }
  14. }
复制代码
对比两种方案,若必要抽取复用的公用组件为单一类型,如Button或Text,保举使用方案二。若必要抽取复用的组件为多个系统组件的组合,如组件中包含Image组件和Text组件,则保举使用方案一。
案例参考

若需抽取一个包含系统组件Image组件和Text组件的公用组件,效果展示如下:
图1 图片和文本组合组件效果


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

  • 提供方封装自定义组件CustomImageText并导出。
  1. @Component
  2. export struct CustomImageText {
  3.   @Prop imageAttribute: AttributeModifier<ImageAttribute>;
  4.   @Prop textAttribute: AttributeModifier<TextAttribute>;
  5.   @Prop imageSrc: PixelMap | ResourceStr | DrawableDescriptor;
  6.   @Prop text: string;
  7.   build() {
  8.     Column() {
  9.       Image(this.imageSrc)
  10.         .attributeModifier(this.imageAttribute)
  11.       Text(this.text)
  12.         .attributeModifier(this.textAttribute)
  13.     }
  14.   }
  15. }
复制代码

  • 使用方分别实现Image组件和Text组件的AttributeModifier接口实现类。
  1. // Image组件的AttributeModifier接口实现类
  2. class ImageModifier implements AttributeModifier<ImageAttribute> {
  3.   private imageWidth: Length = 0;
  4.   private imageHeight: Length = 0;
  5.   constructor(width: Length, height: Length) {
  6.     this.imageWidth = width;
  7.     this.imageHeight = height;
  8.   }
  9.   width(width: Length) {
  10.     this.imageWidth = width;
  11.     return this;
  12.   }
  13.   height(height: Length) {
  14.     this.imageHeight = height;
  15.     return this;
  16.   }
  17.   applyNormalAttribute(instance: ImageAttribute): void {
  18.     instance.width(this.imageWidth);
  19.     instance.height(this.imageHeight);
  20.   }
  21. }
  22. // Text组件的AttributeModifier接口实现类
  23. class TextModifier implements AttributeModifier<TextAttribute> {
  24.   constructor() {
  25.   }
  26.   applyNormalAttribute(instance: TextAttribute): void {
  27.     instance.fontSize(16)
  28.   }
  29. }
复制代码

  • 使用方创建Image组件和Text组件的AttributeModifier接口实现类实例,并作为提供方自定义组件CustomImageText的入参传入。
  1. @Component
  2. struct Index {
  3.   imageAttribute: ImageModifier = new ImageModifier(100, 100);
  4.   textAttribute: TextModifier = new TextModifier();
  5.   build() {
  6.     Row() {
  7.       CustomImageText({
  8.         imageAttribute: this.imageAttribute,
  9.         textAttribute: this.textAttribute,
  10.         imageSrc: $r('app.media.startIcon'),
  11.         text: 'label'
  12.       })
  13.     }
  14.     .justifyContent(FlexAlign.Center)
  15.     .backgroundColor('F1F2F3')
  16.     .width('100%')
  17.     .height('100%')
  18.   }
  19. }
复制代码
弹窗组件封装

使用Dialog弹窗组件场景

如下图所示,团队A是弹窗组件提供方,团队B是弹窗组件使用方。团队A实现了Dialog弹窗模块,并向外提供了相应接口。团队B必要在不同业务场景下展示Dialog弹窗模块中的相应的弹窗组件。比方,团队A封装了警告类型的弹窗组件Dialog并将获取组件的接口暴露给外部,团队B在业务中通过导入并使用该接口打开携带警告信息的弹窗。
图2 使用Dialog弹窗场景

方案先容

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


  • 提供方创建自定义弹窗组件MyCustomDialog。
  1. // 自定义弹窗需要使用@CustomDialog装饰器
  2. @CustomDialog
  3. struct MyCustomDialog {
  4.   @Link visible: boolean;
  5.   // 被@CustomDialog装饰器修饰的组件必须持有CustomDialogController类型属性参数
  6.   controller: CustomDialogController;
  7.   // 弹窗交互事件参数,点击确认和取消按钮时的回调函数
  8.   onCancel?: () => void;
  9.   onConfirm?: () => void;
  10.   build() {
  11.     Column({ space: 12 }) {
  12.       Text("Custom dialog content!")
  13.         .fontSize(20)
  14.       Row() {
  15.         Button("cancel")
  16.           .onClick(() => {
  17.             this.visible = false;
  18.             this.onCancel?.();
  19.           })
  20.           .margin({ right: 20 })
  21.         Button("confirm")
  22.           .onClick(() => {
  23.             this.visible = false;
  24.             this.onConfirm?.();
  25.           })
  26.       }
  27.       .justifyContent(FlexAlign.Center)
  28.     }
  29.     .padding(24)
  30.   }
  31. }
复制代码

  • 提供方创建自定义组件Dialog,该组件是对弹窗组件MyCustomDialog的二次封装,便于外部使用。
  1. @Component
  2. export struct Dialog {
  3.   // 监听外部传入的visible变量,visible值发生变化时触发onChange回调函数
  4.   @Watch("onChange") @Link visible: boolean;
  5.   onCancel?: () => void;
  6.   onConfirm?: () => void;
  7.   // 通过CustomDialogController的builder参数绑定弹窗组件MyCustomDialog
  8.   private controller = new CustomDialogController({
  9.     builder: MyCustomDialog({
  10.       visible: $visible,
  11.       onCancel: this.onCancel,
  12.       onConfirm: this.onConfirm
  13.     })
  14.   })
  15.   // visible的值发生变化时触发,若visible最新值为true通过弹窗控制器打开弹窗,否则关闭弹窗
  16.   onChange() {
  17.     if (this.visible) {
  18.       this.controller.open()
  19.     } else {
  20.       this.controller.close()
  21.     }
  22.   }
  23.   build() {
  24.   }
  25. }
复制代码

  • 使用方导入自定义组件Dialog并传入相应入参。
  1. @Entry
  2. @Component
  3. export struct Index {
  4.   // 外部定义visible变量作为弹窗组件入参,控制弹窗显隐
  5.   @State visible: boolean = false;
  6.   @State openDialogCount: number = 0;
  7.   @State cancelDialogCount: number = 0;
  8.   build() {
  9.     Column({ space: 20 }) {
  10.       Text(`open dialog count: ${this.openDialogCount}`)
  11.         .fontSize(20)
  12.       Text(`cancel dialog count: ${this.cancelDialogCount}`)
  13.         .fontSize(20)
  14.       Button(this.visible ? 'hide' : 'show')
  15.         // 点击修改visible变量后,visible的值可以被Dialog组件监听并响应
  16.         .onClick(() => this.visible = !this.visible)
  17.       // 通过双向绑定visible变量,实现外部控制弹窗
  18.       Dialog({
  19.         visible: $visible,
  20.         onCancel: () => this.cancelDialogCount++,
  21.         onConfirm: () => this.openDialogCount++
  22.       })
  23.     }
  24.     .justifyContent(FlexAlign.Center)
  25.     .width('100%')
  26.     .height('100%')
  27.   }
  28. }
复制代码
  说明
  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方法封装。
  1. @Builder
  2. function MyRadio() {
  3.   Radio({ value: '1', group: 'radioGroup' })
  4.   Radio({ value: '0', group: 'radioGroup' })
  5. }
  6. @Builder
  7. function MyCheckbox() {
  8.   CheckboxGroup({ group: 'checkboxGroup' }) {
  9.     Checkbox({ name: '1', group: 'checkboxGroup' })
  10.     Checkbox({ name: '0', group: 'checkboxGroup' })
  11.   }
  12. }
复制代码

  • 在组件工厂实现方,将封装好的全局@Builder方法使用wrapBuilder函数包裹,并将返回值作为组件工厂Map的value值存入。全部组件存入后,将组件工厂导出供外部使用。
  1. // 定义组件工厂Map
  2. let factoryMap: Map<string, object> = new Map();
  3. // 将需要工厂化的组件存入到组件工厂中
  4. factoryMap.set('Radio', wrapBuilder(MyRadio));
  5. factoryMap.set('Checkbox', wrapBuilder(MyCheckbox));
  6. // 导出组件工厂
  7. export { factoryMap }
复制代码

  • 在使用方,引入组件工厂并通过key值获取对应的WrappedBuilder对象。
  1. // 导入组件工厂,路径需按照实际位置导入,此处仅做示例参考
  2. import { factoryMap } from './ComponentFactory';
  3. // 通过组件工厂Map的key值获取对应的WrappedBuilder对象
  4. let myRadio: WrappedBuilder<[]> = factoryMap.get('Radio') as WrappedBuilder<[]>;
  5. let myCheckbox: WrappedBuilder<[]> = factoryMap.get('Checkbox') as WrappedBuilder<[]>;
复制代码

  • 在使用方的组件build方法中,通过调用WrappedBuilder对象的builder方法获取具体组件。
  1. @Component
  2. struct Index {
  3.   build() {
  4.     Column() {
  5.       // myRadio和myCheckbox是从组件工厂中获取的WrappedBuilder对象
  6.       myRadio.builder();
  7.       myCheckbox.builder();
  8.     }
  9.   }
  10. }
复制代码
  说明
  使用wrapBuilder方法有以下限制:
  

  • wrapBuilder方法只支持传入全局@Builder方法。
  • wrapBuilder方法返回的WrappedBuilder对象的builder属性方法只能在struct内部使用。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

来自云龙湖轮廓分明的月亮

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表