一、前言
当页面有多个相同的UI结构时,若每个都单独声明,同样会有大量重复的代码。为避免重复代码,可以将相同的UI结构提炼为一个自定义组件,完成UI结构的复用。
除此之外,ArkTS
还提供了一种更轻量的UI结构复用机制@Builder
方法,开发者可以将重复使用的UI元素抽象成一个@Builder
方法,该方法可在build()
方法中调用多次,以完成UI结构的复用。
二、自定义构建函数
ArkUI
提供了一种更轻量的UI元素复用机制@Builder
,@Builder
所装饰的函数遵循build()
函数语法规则,开发者可以将重复使用的UI元素抽象成一个方法,在build
方法里调用。
为简化描述,全文将@Builder
装饰的函数也称为“自定义构建函数”。
2.1 自定义组件内自定义构建函数
组件内声明,使用时需要用this
。
定义语法:
@Builder MyBuilderFunction(){}
使用方法:
this.MyBuilderFunction()
@Builder
自定义组件内自定义构建函数使用注意事项如下:
- 允许在自定义组件内定义一个或多个
@Builder
方法,该方法被认为是该组件的私有、特殊类型的成员函数。- 自定义构建函数可以在所属组件的
build
方法和其他自定义构建函数中调用,但不允许在组件外调用。- 在自定义函数体中,
this
指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this
访问自定义组件的状态变量而不是参数传递。
2.2 全局自定义构建函数
全局声明需要加function
关键字,使用时直接写函数名称。
定义语法:
@Builder function MyGlobalBuilderFunction(){}
使用方法:
MyGlobalBuilderFunction()
@Builder
全局自定义构建函数使用注意事项如下:
- 全局的自定义构建函数可以被整个应用获取,不允许使用
this
和bind
方法。- 如果不涉及组件状态变化,建议使用全局的自定义构建方法。
2.3 参数传递规则
自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:
- 参数的类型必须与参数声明的类型一致,不允许
undefined
、null
和返回undefined
、null
的表达式。 - 在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用
@Link
。 @Builder
内UI语法遵循UI语法规则。- 只有传入一个参数,且参数需要直接传入对象字面量才会按引用传递该参数,其余传递方式均为按值传递。
2.3.1 按值传递参数
@Builder
function overBuilder(paramA1: string) {
Row() {
Text(`UseStateVarByValue: ${paramA1} `)
}
}
@Entry
@Component
struct Parent {
@State label: string = 'Hello';
build() {
Column() {
overBuilder(this.label)
}
}
}
2.3.2 按引用传递参数
按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder
方法内的UI刷新。ArkUI
提供$$
作为按引用传递参数的范式。
使用语法如下:
@Builder( $$ : { paramA1: string, paramB1 : string } );
应用示例如下:
@Builder function ABuilder($$:{par: string}){
Row(){
Text( 'abs:' + $$.par)
}
}
@Entry
@Component
struct Index {
@State message: string = 'Hello World'
build() {
Row() {
Column() {
Text(this.message)
.fontSize(50)
.fontWeight(FontWeight.Bold)
ABuilder({par:this.message})
Button('Click Me')
.onClick(() => {
this.message = "哈哈"
})
}
.width('100%')
}
.height('100%')
}
}
或者按照如下方式实现:
@Builder
function ListModel(params:{title:string,count:number}){
Row(){
Text(`${params.title}`)
.height(40)
.textAlign(TextAlign.Center)
.fontColor(Color.White)
Text(`投票人数:${params.count}`)
.fontColor(Color.White)
}
.width("100%")
.height(100)
.backgroundColor(Color.Brown)
.justifyContent(FlexAlign.SpaceAround)
}
@Entry
@Component
struct Index {
@State CountMusic:number=10
build() {
Column({space:10}) {
ListModel({title:"音乐",count:this.CountMusic})
ListModel({title:"美术",count:30})
ListModel({title:"舞蹈",count:12})
Button("增加音乐支持人数").onClick(()=>{
this.CountMusic++;
})
}.width('100%')
.height('100%')
}
}
2.4 @Builder方法和自定义组件的区别
-
@Builder
方法和自定义组件虽然都可以实现UI复用的效果,但是两者还是有着本质区别,其中最为显著的一个区别就是自定义组件可以定义自己的状态变量,而@Builder
方法则不能。 -
若复用的UI结构没有状态,推荐使用
@Builder
方法,否则使用自定义组件。 -
在使用
@Builder
复用逻辑时,可以支持传递参数,从而实现更灵活的UI渲染。 -
参数可以是状态数据,但建议使用对象的方式进行传递(直接传递,无法实现视图更新)。
-
可以使用
Component
来抽象组件,而@Builder
则可以实现轻量级的UI复用。
2.5 @BuilderParam
@BuilderParam
用于装饰自定义组件(struct
)中的属性,其装饰的属性可作为一个UI结构的占位符,待创建该组件时,可通过参数为其传入具体的内容。(其作用类似于Vue
框架中的slot
)。
注意⚠️:@BuilderParam
装饰的方法只能被自定义构建函数(@Builder
装饰的方法)初始化。
应用示例如下:
// ● 标题文字通过属性传入
// ● 内容结构“尾随闭包”传入
@Entry
@Component
struct Index {
build() {
Column({ space: 10 }) {
ListModel({title:"音乐"}){
Text("音乐的内容")
}
ListModel({title:"美术"}){
Text("美术的内容")
}
ListModel({title:"舞蹈"})
}.width('100%').height('100%')
}
}
@Component
struct ListModel {
title: string
// ListContent 名字随便起,默认接收一个自定义构建函数
@BuilderParam ListContent: () => void = this.defaultContent;
@Builder defaultContent () {
Text('默认展示的内容')
}
build() {
Column() {
Text(this.title)
.width('100%')
.height(50)
.textAlign(TextAlign.Center)
this.ListContent();
}.width('100%').height(200).backgroundColor(Color.Brown)
}
}
总结:
- 当子组件使用一个
@BuilderParam
时,使用组件时可以在尾随的大括号{}
中插入UI结构。- 当子组件使用多个
@BuilderParam
时,可以在使用组件时传入参数,例如Comp({ xxx: this.builderFn })
。- 不能既传递默认插槽也传递具名插槽,会报错。
- 子组件本身可以提供一个默认的
@Builder
函数,作为@BuilderParam
的备用函数,用于作为备用内容。
三、应用示例
下面应用List
组件实现图文文章列表布局以及自定义模型类的使用。
在 ArkTS
中定义模型类与在纯 TypeScript
项目中定义类没有本质区别。但是,在 UI 开发中,你可能不会直接在组件模板中使用模型类的实例。相反,你可能会将这些实例绑定到组件的响应式状态变量上,并在模板中使用这些状态变量。这样做的好处是,当状态变量发生变化时,UI 可以自动更新。
示例代码如下:
// 定义模型类
class Item {
id: number;
title: string;
img: ResourceStr;
author: string;
date: string;
constructor(id: number, title: string, img: ResourceStr, author: string, date: string) {
this.id = id
this.title = title
this.img = img
this.author = author
this.date = date
}
}
// 全局自定义构建函数
@Builder function newsItem(item:Item){
Row() {
Column() {
Text(item.title)
.fontSize(16)
.fontWeight(FontWeight.Bold)
.maxLines(2)
.textOverflow({ overflow: TextOverflow.Ellipsis })
Text(`${item.author} ${item.date}`)
.fontSize(14)
}
.layoutWeight(1)
.alignItems(HorizontalAlign.Start)
.height(68)
.justifyContent(FlexAlign.SpaceBetween)
Image(item.img)
.width(100)
.height(68)
.margin({ left: 8 })
.borderRadius(2)
.objectFit(ImageFit.Cover)
}
.width('100%')
.height(80)
}
@Entry
@Component
struct NewsBuilder {
@State articleList: Item[] = [
new Item(1, "嫦娥六号探测器进入环月轨道飞行", "https://p5.img.cctvpic.com/photoworkspace/contentimg/2024/05/08/2024050820144382684.jpg", "央视网", "2014-05-08"),
new Item(2, "“五一”假期游处处火爆 有哪些文旅新潮流?", "https://cms-emer-res.cctvnews.cctv.com/image/1005/upload/17292d188bc64cef882f1cd07d3d2acc.jpeg", "央视新闻客户端", "2024-05-08"),
new Item(3, "杭州:全面取消住房限购,购房即可申请落户", $r("app.media.2"), "界面新闻", "2024-05-09"),
new Item(4, "时隔五年,合肥或将再次引进“国宝”大熊猫", "https://pics4.baidu.com/feed/d50735fae6cd7b899b1d7bef7ec7b8aad8330e5d.jpeg@f_auto?token=e70fa976dceb8e88433c493f7e86b88c", "九派新闻", "2024-05-08"),
new Item(5, "发布擦边广告再被罚,椰树集团徘徊在“土味”和“低俗”之间?", "https://pics1.baidu.com/feed/e824b899a9014c083cd8d85d067bf9057af4f471.jpeg@f_auto?token=c6770b5d0d99a68721fc88e7a337d01c", "北京商报", "2024-05-08"),
new Item(6, "特斯拉在华推进全自动驾驶 智能网联车产业链迎新机遇", $r("app.media.2"), "每日经济新闻", "2024-05-09"),
]
build() {
Column() {
List() {
ForEach(this.articleList, (item: Item) => {
ListItem() {
// 按值传递参数
newsItem(item)
}
.padding({ top: 5, bottom: 5 })
})
}
.divider({ strokeWidth: 1, color: "#eeeeee" })
.backgroundColor(0xffffff)
.borderRadius(10)
.padding(10)
}.padding(10)
.backgroundColor(0xeeeeee)
.width('100%')
.height('100%')
}
}
效果图如下: