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 function name () {}

使用@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%')

  }

}

实现效果,如图所示。

  1. 案例

封装 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'

    })

  }

}

欢迎加入课程班级,考取鸿蒙认证:

https://developer.huawei.com/consumer/cn/training/classDetail/d43582bb30b34f548c16c127cb3be104?type=1?ha_source=hmosclass&ha_sourceId=89000248

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐