HarmonyOS之ArkUI组件封装最佳实践
一、背景与案例描述
在应用开发中,对一些频繁使用的业务UI组件常常会进行一层封装,提取到公共基础库中实现组件的复用,避免类似的逻辑重复编写,减少代码冗余,从而提高开发效率,同时也降低了业务模块间的耦合,可维护性与扩展性会更强,其他开发者在需要时,只需简单地调用或实现这些组件提供的接口,即可快速完成所需功能的开发,很大程度上可以提高团队的效率和代码质量。
在ArkUI中定义一个组件是很简单的,通过@Component
装饰器、struct
关键字修饰即可,如下:
@Component export struct Toolbar { build(){ } }
下面写一个Toolbar组件为例,如何循序渐进封装一个UI组件,做到灵活定制,可复用性强的组件,效果图:
Toolbar组件中有四个元素,分别是返回、标题、分享、更多,其中标题是Text
组件,其他元素是Image
组件实现,是可点击的。
二、静态注册属性-封装UI组件
首先定义对外的成员属性,用@Prop
装饰器声明用于接收父组件的参数。
@Component export struct Toolbar { // 注释1 @Prop title: string | ResourceStr @Prop titleColor: ResourceStr | Color @Prop titleSize: ResourceStr | number // 注释2 onBack: Function = () => { } onMore?: () => void onShare?:() => void }
注释1:对外的公开属性,接收来自父组件的参数
注释2:点击事件的回调方法,让定制业务逻辑交给使用者去实现。
然后在build()
方法中实现UI布局,如下:
build() { Row() { Image($r('app.media.ic_toolbar_back')) // 注释1 .imageStyle() .onClick(() => { this.onBack() }) Text(this.title) .textAlign(TextAlign.Start) .layoutWeight(1) .fontColor(this.titleColor) .padding({ left: '10vp', right: '10vp' }) .fontSize(this.titleSize) Image($r('app.media.ic_toolbar_share')) // 注释1 .imageStyle() .onClick(()=>{ if (this.onShare) this.onShare() }) Image($r('app.media.ic_toolbar_more')) // 注释1 .imageStyle() .margin({ left: '12vp' }) .onClick(()=>{ if (this.onMore) this.onMore() }) } .backgroundColor(Color.Pink) .padding({ left: '10vp', right: '15vp' }) .height('50vp') .width('100%') }
注释1: Image组件的公共样式,通过
@Extend
装饰器进行了抽离成函数,其函数必须定义在文件顶层作用域中,不能在类中定义。
@Extend(Image) function imageStyle(){ .size({ height: '100%', width: '25vp' }) .objectFit(ImageFit.Auto) }
上面就完成了Toolbar组件常规封装了,导入使用传入参数。
Toolbar({ title: '标题', titleSize: '25vp', titleColor: Color.Black, onBack: () => { router.back() } })
疑问❓:你会发现上面的封装有什么缺陷吗,如果封装组件需要支持样式的动态化时,此时就要在封装组件中新增对应的成员属性,比如在Toolbar组件中,需要修改标题对齐方式、字体粗细等等 ,就需要定义这些特定的属性。如果封装组件是一个组合式组件(由多个组件组合实现),每个类型组件都有自己特有属性,这样整合起来是不是要新增很多成员属性来接收外部的参数呢。
以Text组件的属性为例,如果定义这么多成员属性来接收外部的参数,是一个地狱性的设计。
这种静态注册属性的方式显然是不太合理的,在处理属性动态化有点力不从心,其扩展性和可维护性差,如果你是封装一个对外开源UI组件,开发者的需求是多样性的,是很难满足个性化需求的。
对此有没更佳的解决方案呢,当然有,系统为每个组件提供一个attributeModifier
属性方法,正好可以解决此类的问题。
三、动态注册属性-封装UI组件
通过AttributeModifier
来动态注册属性的方式来封装UI组件,解决静态注册属性的问题,改方式可以将属性从组件分离解耦出来,由外部使用者按需设置。
先初步了解下AttributeModifier
接口的属性方法。
- applyNormalAttribute:定义组件正常状态的属性值
- applyPressedAttribute:定义组件按下的属性,比如点击事件
- applyFocusedAttribute:定义焦点相关的属性
- applyDisabledAttribute:定义组件禁用属性
- applySelectedAttribute:定义选择属性
接下来通过AttributeModifier
来改造Toolbar封装组。
🌾 1. 在封装组件中定义对应的Modifier属性
export struct Toolbar2 { @Prop title: StringOrNull // 注释1 @Prop titleModifier: AttributeModifierOrNull<TextAttribute> @Prop backModifier: AttributeModifierOrNull<ImageAttribute> @Prop shareModifier: AttributeModifierOrNull<ImageAttribute> @Prop moreModifier: AttributeModifierOrNull<ImageAttribute>
🔈:注释1:处从上到下分别是标题、返回、分享、更多按钮的
AttributeModifier
成员属性,接收外部传入的AttributeModifier
类实例。其中AttributeModifierOrNull
是自定义的别名联合类型。
export type AttributeModifierOrNull<T> = AttributeModifier<T> | null
🌾2. 接着自定义实现类来实现AttributeModifier
接口,在实现类可以定义set方法来实现链式调用方式设置属性。这样我们定义一个BaseModifier
基础类来实现AttributeModifier
接口,主要是为了后期扩展。
class BaseModifier <T> implements AttributeModifier<T> { applyNormalAttribute(instance: T): void { } }
然后在封装组件Toolbar中的aboutToAppear()
方法中初始化默认值。
🔈:注意:如果不初始化默认值,如果外部没有传入对应的Modifier实例就会抛出异常闪退。
aboutToAppear(): void { if (!this.titleModifier) { this.titleModifier = new BaseModifier<TextAttribute>() } if (!this.backModifier) { this.backModifier = new BaseModifier<ImageAttribute>() } if (!this.shareModifier) { this.shareModifier = new BaseModifier<ImageAttribute>() } if (!this.moreModifier) { this.moreModifier = new BaseModifier<ImageAttribute>() } }
最后将成员变量Modifier属性关联到对应的组件中,如下:
Image($r('app.media.ic_toolbar_back')) .imageStyle() // 注释1 .attributeModifier(this.backModifier) Text(this.title) .textAlign(TextAlign.Start) .layoutWeight(1) .padding({ left: '10vp', right: '10vp' }) // 注释1 .attributeModifier(this.titleModifier) Image($r('app.media.ic_toolbar_share')) .attributeModifier(this.shareModifier) .imageStyle() Image($r('app.media.ic_toolbar_more')) .imageStyle() .margin({ left: '12vp' }) // 注释1 .attributeModifier(this.moreModifier)
🔈:注释1: 处便是Modifier属性的设置,建议将设置关联
attributeModifie()
方法放在组件的最后调用链处,这样外部设置可以覆盖掉组件内的属性值。
🌾 3、外部使用者自定义类实现AttributeModifier
接口,或者继承BaseModifier
基础类。
- 自定义实现
AttributeModifier
接口
class TitleModifier implements AttributeModifier<TextAttribute> { private _fontWeight?: FontWeight = FontWeight.Normal private _textAlign?: TextAlign = TextAlign.Center private _fontSize?: string | undefined = '25vp' private _fontColor?: Color = Color.Blue public setFontSize(value: string | undefined) { this._fontSize = value } public setFontWeight(value: FontWeight): TitleModifier { this._fontWeight = value return this } public setTextAlign(value: TextAlign): TitleModifier { this._textAlign = value return this } public setFontColor(value: Color): TitleModifier { this._fontColor = value return this } applyNormalAttribute(instance: TextAttribute): void { instance.fontColor(this._fontColor) instance.fontSize(this._fontSize) instance.textAlign(this._textAlign) instance.fontWeight(this._fontWeight) } }
BaseModifier
基础类
class BackModifier extends BaseModifier<ImageAttribute> { private static instance: BackModifier public static getInstance(): BackModifier { if (BackModifier.instance) { return BackModifier.instance } else { return new BackModifier() } } applyPressedAttribute(instance: ImageAttribute): void { // 点击返回 router.back() } }
最后是外部使用TitleModifier和BackModifier,
Toolbar2({ title: '标题', backModifier: BackModifier.getInstance(), titleModifier: new TitleModifier() .setFontWeight(FontWeight.Regular) .setFontColor(Color.Red) }).margin({ top: '30vp' }) Toolbar2({ title: '标题2', backModifier: BackModifier.getInstance(), titleModifier: new TitleModifier() .setFontWeight(FontWeight.Bold) .setFontColor(Color.Orange) }).margin({ top: '30vp' })
效果图:
🔈:其中
BackModifier
是一个单例模式,如果多处调用可以避免创建多个实例带来的性能损耗,在组件数量少可能很难体现这价值,如果是在长列表场景中可能会体现出来。
四、总结
静态注册属性封装UI组件的特点:使用简单,由于要手动定义属性,导致在可维护性和可扩展性上有很大的局限性,对于简单的UI组件,静态注册可能是可行的封装方式,易用易理解,但是比较复杂的组合式ui组件,尤其是那些对动态设置要求较高的场景时,静态注册就显得不那么适用了,需要定义众多的成员属性,无法根据实际需求灵活地按需注册属性。
动态注册属性封装UI组件的特点:是通过组件的AttributeModifier来实现的,相较于静态注册方式,尽管操作更为复杂,但可以弥补静态注册的缺陷。这种方法可以按需设置属性,从而体现出极高的灵活性和扩展性,充分体现了封装的精髓,同时也遵循了单一职责设计原则,功能清晰划分明了。