
HarmonyOS自定义构建函数 原创
1. 构建函数-@Builder
如果不想使用 @Component 直接抽象组件,ArkUI还提供了一种更轻量的UI元素复用机制 @Builder,可以将重复使用的UI元素抽象成一个方法,在 build 方法里调用。称之为自定义构建函数。
用法- 可以使用 @Builder 修饰符进行修饰。
例如上面图片设置页面的每个操作项,可以单独的抽离出来,进行复用。
案例代码如下:
@Entry
@Component
struct Index {
build() {
Column(){
Row(){
Row(){
Text('语言切换')
Text('中文')
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({left: 15, right: 15})
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({left: 10, right: 10})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#ccc')
}
}
实现效果,如图所示。
假设有N个这样的单个元素,但是重复的去写会浪费大量的代码,丧失代码的可读性,此时我们就可以使用builder构建函数。
1.1 全局定义- @Builder functionname () {}
使用@Builder抽取组件代码,如下所示
@Builder
function getItem(leftStr: string, rightStr: string) {
Row() {
Row() {
Text(leftStr)
Text(rightStr)
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({ left: 10, right: 10 })
}
在组件中使用,完整代码如下:
@Builder
function getItem(leftStr: string, rightStr: string) {
Row() {
Row() {
Text(leftStr)
Text(rightStr)
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({ left: 10, right: 10 })
}
@Entry
@Component
struct BuilderCase {
build() {
Column({ space: 10 }) {
getItem('昵称', 'Mark')
getItem('语言切换', '中文')
getItem('位置设置', '深圳')
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#ccc')
}
}
实现效果,如图所示。
全局自定义函数的问题
· 全局的自定义构建函数可以被整个应用获取,不允许使用this和bind方法。
· 如果不涉及组件状态变化,建议使用全局的自定义构建方法。
· 补一句-如果数据是响应式的-此时该函数不会自动渲染-哪怕是全局自定义函数,不可被其他文件引用。
将数据声明为State响应式数据
interface IFormatData{
nickname: string
lang: string
address: string
}
export class IFormatDataModel implements IFormatData {
nickname: string = ''
lang: string = ''
address: string = ''
constructor(model: IFormatData) {
this.nickname = model.nickname
this.lang = model.lang
this.address = model.address
}
}
@State
formatData: IFormatDataModel = new IFormatDataModel({nickname:'mark', lang: '中文', address: '深圳' })
传递数据,绑定为对应字段,代码如下:
interface IFormatData{
nickname: string
lang: string
address: string
}
export class IFormatDataModel implements IFormatData {
nickname: string = ''
lang: string = ''
address: string = ''
constructor(model: IFormatData) {
this.nickname = model.nickname
this.lang = model.lang
this.address = model.address
}
}
@Entry
@Component
struct BuilderStateCase {
@State
formatData: IFormatDataModel = new IFormatDataModel({nickname:'mark', lang: '中文', address: '深圳' })
build() {
Column({ space: 10 }) {
getItem('昵称', this.formatData.nickname)
getItem('语言切换', this.formatData.lang)
getItem('位置设置', this.formatData.address)
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#ccc')
}
}
修改响应式数据,完整代码如下:
interface IFormatData{
nickname: string
lang: string
address: string
}
export class IFormatDataModel implements IFormatData {
nickname: string = ''
lang: string = ''
address: string = ''
constructor(model: IFormatData) {
this.nickname = model.nickname
this.lang = model.lang
this.address = model.address
}
}
@Builder
function getItem(leftStr: string, rightStr: string) {
Row() {
Row() {
Text(leftStr)
Text(rightStr)
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({ left: 10, right: 10 })
}
@Entry
@Component
struct BuilderStateCase {
@State
formatData: IFormatDataModel = new IFormatDataModel({nickname:'mark', lang: '中文', address: '广州' })
build() {
Column({ space: 10 }) {
getItem('昵称', this.formatData.nickname)
getItem('语言切换', this.formatData.lang)
getItem('位置设置', this.formatData.address)
Text(JSON.stringify(this.formatData))
Button('修改数据-语言切换')
.onClick(()=>{
this.formatData.lang = this.formatData.lang == '英文' ? '中文' : '英文'
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#ccc')
}
}
实现效果,如图所示。
我们发现,点击修改数据-语言切换按钮是没有任何反应的,说明此时即使用了State,但是此时的全局builder依然不更新。
那怎么办?我们试试在组件内部定义。
1.2 组件内定义- 语法 @Builder name () {}
把@Builder标注的代码部分,放置到组件内部,完整代码如下:
interface IFormatData{
nickname: string
lang: string
address: string
}
export class IFormatDataModel implements IFormatData {
nickname: string = ''
lang: string = ''
address: string = ''
constructor(model: IFormatData) {
this.nickname = model.nickname
this.lang = model.lang
this.address = model.address
}
}
@Entry
@Component
struct BuilderCase_2 {
@State
formatData: IFormatDataModel = new IFormatDataModel({ nickname: 'mark', lang: '中文', address: '广州' })
@Builder
getItem(leftStr: string, rightStr: string) {
Row() {
Row() {
Text(leftStr)
Text(rightStr)
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({ left: 10, right: 10 })
}
build() {
Column({ space: 10 }) {
this.getItem('昵称', this.formatData.nickname)
this.getItem('语言切换', this.formatData.lang)
this.getItem('位置设置', this.formatData.address)
Text(JSON.stringify(this.formatData))
Button('修改数据-语言切换')
.onClick(() => {
this.formatData.lang = this.formatData.lang == '英文' ? '中文' : '英文'
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#ccc')
}
}
实现效果,如图所示:
调用多了this,其他和全局属性一样,没有任何变化,此时我们发现修改数据依然没有任何变化,这是为什么呢?
注意:我们刚刚传过去的是什么类型,string是一个基础数据类型,它是按值传递的,不具备响应式更新的特点。
总结
全局Builder函数和组件Builder构建函数可以实现一种轻量级的UI复用。
区别: 全局自定义构建函数不允许使用this,bind,它适合一种纯渲染的UI结构。
组件内自定义Builder可以实现this调用。
2. 构建函数-传参传递
自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:
参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
在自定义构建函数内部,不允许改变参数值。如果需要改变参数值,且同步回调用点,建议使用@Link。
@Builder内UI语法遵循UI语法规则。
我们发现上一个案例,使用了string这种基础数据类型,即使它属于用State修饰的变量,也不会引起UI的变化。
按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder方法内的UI刷新。ArkUI提供$$作为按引用传递参数的范式。
格式如下:
SomeBuilder( $$ : 类型);
也就是我们需要在builder中传入一个对象,该对象使用$$(可使用其他字符)的符号来修饰,此时数据具备响应式了。
定义IOptions接口,在@Builder中传入IOptions接口。
interface IOptions {
leftStr: string
rightStr: string
}
@Builder
getItem($$: IOptions) {
Row() {
Row() {
Text($$.leftStr)
Text($$.rightStr)
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({ left: 10, right: 10 })
}
传递参数值,代码如下:
this.getItem({ leftStr: '昵称', rightStr:this.formatData.nickname })
this.getItem({ leftStr: '语言切换', rightStr:this.formatData.lang })
this.getItem({ leftStr: '位置设置', rightStr:this.formatData.address })
修改后的完整代码如下;
interface IFormatData{
nickname: string
lang: string
address: string
}
export class IFormatDataModel implements IFormatData {
nickname: string = ''
lang: string = ''
address: string = ''
constructor(model: IFormatData) {
this.nickname = model.nickname
this.lang = model.lang
this.address = model.address
}
}
interface IOptions {
leftStr: string
rightStr: string
}
@Entry
@Component
struct ParamCase{
@State
formatData: IFormatDataModel = new IFormatDataModel({ nickname: 'mark', lang: '中文', address: '深圳' })
@Builder
getItem($$: IOptions) {
Row() {
Row() {
Text($$.leftStr)
Text($$.rightStr)
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({ left: 10, right: 10 })
}
build() {
Column({ space: 10 }) {
this.getItem({ leftStr: '昵称', rightStr: this.formatData.nickname })
this.getItem({ leftStr: '语言切换', rightStr: this.formatData.lang })
this.getItem({ leftStr: '位置设置', rightStr: this.formatData.address })
Text(JSON.stringify(this.formatData))
Button('修改数据-语言切换')
.onClick(() => {
this.formatData.lang = this.formatData.lang == '英文' ? '中文' : '英文'
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#ccc')
}
}
实现效果,如图所示。
这里,点击“修改数据-语言切换”按钮,语言切换的数据发生改变。
同样的,全局 Builder 也支持这种用法,完整代码如下。
interface IFormatData {
nickname: string
lang: string
address: string
}
export class IFormatDataModel implements IFormatData {
nickname: string = ''
lang: string = ''
address: string = ''
constructor(model: IFormatData) {
this.nickname = model.nickname
this.lang = model.lang
this.address = model.address
}
}
interface IOptions {
leftStr: string
rightStr: string
}
@Builder
function getItem(options: IOptions) {
Row() {
Row() {
Text(options.leftStr)
Text(options.rightStr)
}
.width('100%')
.height(40)
.borderRadius(10)
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 15, right: 15 })
.backgroundColor(Color.White)
}
.width('100%')
.height(40)
.padding({ left: 10, right: 10 })
}
@Entry
@Component
struct ParamCase_2 {
@State
formatData: IFormatDataModel = new IFormatDataModel({ nickname: 'mark', lang: '中文', address: '深圳' })
build() {
Column({ space: 10 }) {
getItem({ leftStr: '昵称', rightStr: this.formatData.nickname })
getItem({ leftStr: '语言切换', rightStr: this.formatData.lang })
getItem({ leftStr: '位置设置', rightStr: this.formatData.address })
Text(JSON.stringify(this.formatData))
Button('修改数据-语言切换')
.onClick(() => {
this.formatData.lang = this.formatData.lang == '英文' ? '中文' : '英文'
})
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.backgroundColor('#ccc')
}
}
使用 @Builder 复用逻辑的时候,支持传参可以更灵活的渲染UI。
参数可以使用状态数据,不过建议通过对象的方式传入 @Builder。
3. 构建函数-@BuilderParam 传递UI
Component可以抽提组件。
Builder可以实现轻量级的UI复用。
完善了吗?其实还不算,比如下面这个例子,如图所示。
大家发现没有,项目中会有很多地方用到这种类似卡片 Card 的地方,里面的内容各有不同,怎么办?
在 Vue 里面有个叫做 slot 插槽的东西,就是可以传入自定义的结构,整体复用父组件的外观。
ArkTS提供了一个叫做 BuilderParam 的修饰符,可以在组件中定义这样一个函数属性,在使用组件时直接传入。
BuilderParam只能应用在 Component 组件中,不能使用 Entry 修饰的组件中使用。
语法:
@BuilderParam name: () => void
声明一个HmCard组件
@Component
struct HmCard {
@BuilderParam
content?: () => void
build() {
Column() {
Text("卡片组件")
Divider()
Row() {
Text("传入内容:")
if (this.content) {
this.content()
}
}
}
.width('100%')
.height(400)
.border({ width: 1 })
.borderRadius(10)
.backgroundColor('#ccc')
}
}
父组件调用传入
@Entry
@Component
struct BuilderParamCase{
@Builder
getContent() {
Row() {
Text("插槽内容")
.fontColor(Color.Red)
}
}
build() {
Column() {
HmCard({ content: this.getContent })
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
.padding(10)
}
}
完整代码如下:
@Component
struct HmCard {
@BuilderParam
content?: () => void
build() {
Column() {
Text("卡片组件")
Divider()
Row() {
Text("传入内容:")
if (this.content) {
this.content()
}
}
}
.width('100%')
.height(400)
.border({ width: 1 })
.borderRadius(10)
.backgroundColor('#ccc')
}
}
@Entry
@Component
struct BuilderParamCase{
@Builder
getContent() {
Row() {
Text("插槽内容")
.fontColor(Color.Red)
}
}
build() {
Column() {
HmCard({ content: this.getContent })
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
.padding(10)
}
}
运行效果,如图所示。
需要注意的是,传入的函数必须是使用 Builder 修饰符修饰的。
BuilderParams类似于 Vue 中的插槽。
(1)子组件中定义一个用 BuilderParam 修饰的函数。
(2)父组件需要给子组件传入一个用 Builder 修饰的函数来赋值给子组件。
(3)子组件要在想要显示插槽的地方来调用传入的方法。
插槽默认值
当我们的调用组件的时候,如果没有给插槽传递参数值,则这个时候我们可以给 BuilderParam 函数默认值,代码如下。
@Component
struct HmCard2{
@Builder
contentDefault(){
Text('我是插槽的默认值')
.fontColor(Color.Green)
}
@BuilderParam
content: () => void = this.contentDefault
build() {
Column() {
Text("卡片组件")
Divider()
Row() {
Text("传入内容:")
if (this.content) {
this.content()
}
}
}
.width('100%')
.height(400)
.border({ width: 1 })
.borderRadius(10)
.backgroundColor('#ccc')
}
}
@Entry
@Component
struct BuilderParamCase_2 {
@Builder
getContent() {
Row() {
Text("插槽内容")
.fontColor(Color.Red)
}
}
build() {
Column() {
HmCard2()
}
.justifyContent(FlexAlign.Center)
.width('100%')
.height('100%')
.padding(10)
}
}
实现效果,如图所示:
尾随闭包
当我们的组件只有一个BuilderParam 的时候,此时可以使用尾随闭包的语法也就是像我们原来使用Column或者Row组件时一样,直接在大括号中传入,代码格式如下:
HmCard3() {
Text("插槽内容-尾随闭包的写法")
.fontColor(Color.Red)
this.getContent()
}
如果有多个呢,不好意思,必须在组件的函数中老老实实的传入多个builder自定义函数。
完整代码如下:
@Component
struct HmCard3 {
@BuilderParam
content?: () => void
@BuilderParam
header?: () => void
build() {
Column () {
Text("卡片组件")
if(this.header) {
this.header()
}
Divider()
Text("传入内容")
if(this.content) {
this.content()
}
}
}
}
@Entry
@Component
struct BuilderParamCase_3 {
@Builder
getContent () {
Row() {
Text("插槽内容")
.fontColor(Color.Red)
}
}
@Builder
getHeader () {
Row() {
Text("头部内容")
.fontColor(Color.Red)
}
}
build() {
Row() {
Column() {
HmCard3({
header: () => {
this.getHeader()
},
content: () => {
this.getContent()
}
})
}
.width('100%')
}
.height('100%')
}
}
实现效果,如图所示。
4. 案例
封装 HmCard 和 HmCardItem 组件,使用 BuilderParam 属性完成如下效果图。
完整代码如下:
@Entry
@Component
struct BuilderParamCardCase {
build() {
Column() {
HmCard4() {
HmCardItem({ leftStr: '员工姓名', rightStr: '周星星' })
HmCardItem({ leftStr: '员工编号', rightStr: '9527' })
HmCardItem({ leftStr: '员工权限', rightStr: '普通' })
HmCardItem({ leftStr: '员工组织', rightStr: '研发部' })
}
}
.height('100%')
.backgroundColor("#ccc")
}
}
@Component
struct HmCard4 {
@BuilderParam
CardContent?: () => void
build() {
Column() {
Column() {
if (this.CardContent) {
this.CardContent()
}
}.borderRadius(8)
.backgroundColor(Color.White)
}.padding({
left: 15,
right: 15
})
.margin({
top: 10
})
}
}
@Component
struct HmCardItem {
leftStr: string = ''
rightStr: string = ''
build() {
Row() {
Text(this.leftStr)
Text(this.rightStr).fontColor("#ccc")
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({
left: 10,
right: 10
})
.height(50)
.border({
width: {
bottom: 1
},
color: '#f4f5f6'
})
}
}
