23、鸿蒙学习——@Builder装饰器(自定义构建函数)

1、装饰器使用说明

@Builder装饰器有两种使用方式,分别是定义在自定义组件内部的私有自定义构建函数和定义在全局的全局自定义构建函数

(1)私有自定义函数

示例:

复制代码
@Entry
@Component
struct BuilderDemo {
  @Builder
  showTextBuilder() {
    // @Builder装饰此函数,使其能以链式调用的方式配置并构建Text组件
    Text('Hello World')
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }


  @Builder
  showTextValueBuilder(param: string) {
    Text(param)
      .fontSize(30)
      .fontWeight(FontWeight.Bold)
  }


  build() {
    Column() {
      // 无参数
      this.showTextBuilder()
      // 有参数
      this.showTextValueBuilder('Hello @Builder')
    }
  }
}

使用方法:

  • 允许在自定义组件内定义一个或多个@Builder函数,该函数被认为是该组件的私有、特殊类型的成员函数
  • 私有自定义构建函数允许在自定义组件内、build函数和其他自定义构建函数中使用
  • 在自定义组件中,this指代当前所属组件,组件的状态变量可在自定义构建函数内访问。建议通过this访问组件的状态变量,而不是通过参数传递。

(2)全局自定义构建函数

示例:

复制代码
@Builder
function showTextBuilder() {
  Text('Hello World')
    .fontSize(30)
    .fontWeight(FontWeight.Bold)
}
@Entry
@Component
struct BuilderDemo {
  build() {
    Column() {
      showTextBuilder()
    }
  }
}
  • 如果不涉及组件状态变化,建议使用全局的自定义构建函数。
  • 全局自定义的构建函数允许在build函数和其他自定义构建函数中调用。

2、参数传递规则

自定义构建函数的参数传递有按值传递和按引用传递两种,均需遵守以下规则:

  • 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
  • 在@Builder装饰的函数内部,不允许改变参数值。
  • @Builder内UI语法遵循UI语法规则
  • 只有当传入一个参数且该参数直接传入对象字面量时,才会按引用传递,其他传递方式均为按值传递。

(1)按值传递

调用@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起@Builder函数内的UI刷新。所以当使用状态变量的时候,推荐使用按引用传递。

复制代码
@Builder function overBuilder(paramA1: string) {
  Row() {
    Text(`UseStateVarByValue: ${paramA1} `)
  }
}
@Entry
@Component
struct Parent {
  @State label: string = 'Hello';
  build() {
    Column() {
      overBuilder(this.label)
    }
  }
}

(2)按引用传递参数

按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起@Builder函数内的UI刷新。

复制代码
class Tmp {
  paramA1: string = '';
}


@Builder
function overBuilder(params: Tmp) {
  Row() {
    Text(`UseStateVarByReference: ${params.paramA1} `)
  }
}


@Entry
@Component
struct Parent {
  @State label: string = 'Hello';


  build() {
    Column() {
      // 在父组件中调用overBuilder组件时,
      // 把this.label通过引用传递的方式传给overBuilder组件。
      overBuilder({ paramA1: this.label })
      Button('Click me').onClick(() => {
        // 单击Click me后,UI文本从Hello更改为ArkUI。
        this.label = 'ArkUI';
      })
    }
  }
}
限制条件
  1. @Builder装饰的函数内部不允许修改参数值,否则框架会抛出运行时错误。但开发者可以在使用@Builder的自定义组件中改变其参数。请参考在@Builder装饰的函数内部修改入参内容
  2. @Builder按引用传递且仅传入一个参数时,才会触发动态渲染UI。
  3. 如果@Builder传入的参数是两个或两个以上,不会触发动态渲染UI。
  4. @Builder传入的参数中同时包含按值传递和按引用传递,不会触发动态渲染UI
  5. @Builder的参数必须按照对象字面量的形式,把所属属性一一传入,才会触发动态渲染UI。

3、 @LocalBuilder和@Builder区别说明

如下:当函数componentBuilder被@Builder修饰时,显示效果为"Child";当函数ComponentBuilder被@LocalBuilder修饰时,显示效果是"Parent"。

复制代码
@Component
struct Child {
  label: string = 'Child';
  @BuilderParam customBuilderParam: () => void;


  build() {
    Column() {
      this.customBuilderParam()
    }
  }
}


@Entry
@Component
struct Parent {
  label: string = 'Parent';


  @Builder componentBuilder() {
    Text(`${this.label}`)
  }


  // @LocalBuilder componentBuilder() {
  //   Text(`${this.label}`)
  // }


  build() {
    Column() {
      Child({ customBuilderParam: this.componentBuilder })
    }
  }
}

说明:

  • @Builder componentBuilder()通过this.componentBuilder的形式传给子组件@BuilderParam customBuilderParam,this指向子组件Child的实例。
  • @LocalBuilder componentBuilder() 通过this.componentBuilder的形式传给子组件@BuilderParam customBuilderParam,this指向父组件Parent的实例。

4、使用@BuilderParam 时,this的指向问题

this的指向示例如下:

复制代码
@Component
struct Child {
  label: string = 'Child';


  @Builder
  customBuilder() {
  }


  @Builder
  customChangeThisBuilder() {
  }


  @BuilderParam customBuilderParam: () => void = this.customBuilder;
  @BuilderParam customChangeThisBuilderParam: () => void = this.customChangeThisBuilder;


  build() {
    Column() {
      this.customBuilderParam()
      this.customChangeThisBuilderParam()
    }
  }
}


@Entry
@Component
struct Parent {
  label: string = 'Parent';


  @Builder
  componentBuilder() {
    Text(`${this.label}`)
  }


  build() {
    Column() {
      // 调用this.componentBuilder()时,this指向当前@Entry所装饰的Parent组件,即label变量的值为"Parent"。
      this.componentBuilder()
      Child({
        // 把this.componentBuilder传给子组件Child的@BuilderParam customBuilderParam,this指向的是子组件Child,即label变量的值为"Child"。
        customBuilderParam: this.componentBuilder,
        // 把():void=>{this.componentBuilder()}传给子组件Child的@BuilderParam customChangeThisBuilderParam,
        // 因为箭头函数的this指向的是宿主对象,所以label变量的值为"Parent"。
        customChangeThisBuilderParam: (): void => {
          this.componentBuilder()
        }
      })
    }
  }
}

示例效果如下:

限制条件

  • 使用@BuilderParam装饰的变量只能通过@Builder函数进行初始化。
  • 当@Require装饰器和@BuilderParam装饰器一起使用时,@BuilderParam装饰器必须进行初始化。
  • 在自定义组件尾随闭包的场景下,子组件有且仅有一个@BuilderParam用来接收此尾随闭包,且此@BuilderParam不能有参数。

5、尾随闭包初始化组件

在自定义组件中,使用@BuilderParam装饰的属性可通过尾随闭包进行初始化。初始化时,组件后需紧跟一个大括号"{}"形成尾随闭包场景。

说明

  • 此场景下自定义组件内仅有一个使用@BuilderParam装饰的属性
  • 此场景下自定义组件不支持通用属性。

开发者可以将尾随闭包内的内容看作@Builder装饰的函数传给@BuilderParam。

示例1:

复制代码
@Component
struct CustomContainer {
  @Prop header: string = '';


  @Builder
  closerBuilder() {
  }


  // 使用父组件的尾随闭包{}(@Builder装饰的方法)初始化子组件@BuilderParam
  @BuilderParam closer: () => void = this.closerBuilder;


  build() {
    Column() {
      Text(this.header)
        .fontSize(30)
      this.closer()
    }
  }
}


@Builder
function specificParam(label1: string, label2: string) {
  Column() {
    Text(label1)
      .fontSize(30)
    Text(label2)
      .fontSize(30)
  }
}


@Entry
@Component
struct CustomContainerUser {
  @State text: string = 'header';


  build() {
    Column() {
      // 创建CustomContainer,在创建CustomContainer时,通过其后紧跟一个大括号"{}"形成尾随闭包
      // 作为传递给子组件CustomContainer @BuilderParam closer: () => void的参数
      CustomContainer({ header: this.text }) {
        Column() {
          specificParam('testA', 'testB')
        }.backgroundColor(Color.Yellow)
        .onClick(() => {
          this.text = 'changeHeader';
        })
      }
    }
  }
}

示例效果图:

6、wrapperBuilder(封装全局@Builder)的使用

接口说明

wrapBuilder是一个模版函数,返回一个WrappedBuilder对象。

复制代码
declare function wrapBuilder<Args extends Object[]>(builder: (...args: Args) => void): WrappedBuilder<Args>;

同时 WrappedBuilder对象也是一个模版类。

复制代码
declare class WrappedBuilder<Args extends Object[]> {
  builder: (...args: Args) => void;


  constructor(builder: (...args: Args) => void);
}

说明:模版参数Args extends Object[] 是需要包装的builder函数的参数列表

使用方法:

复制代码
let builderVar: WrappedBuilder<[string, number]> = wrapBuilder(MyBuilder);
let builderArr: WrappedBuilder<[string, number]>[] = [wrapBuilder(MyBuilder)]; //可以放入数组

限制条件

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

@Builder方法赋值给变量

使用@Builder装饰器装饰的方法MyBuilder作为wrapBuilder的参数,再将wrapBuilder函数的返回值赋值给变量globalBuilder,以解决@Builder方法赋值给变量后无法使用的问题。

复制代码
@Builder
function MyBuilder(value: string, size: number) {
  Text(value)
    .fontSize(size)
}


let globalBuilder: WrappedBuilder<[string, number]> = wrapBuilder(MyBuilder);


@Entry
@Component
struct Index {
  @State message: string = 'Hello World';


  build() {
    Row() {
      Column() {
        globalBuilder.builder(this.message, 50)
      }
      .width('100%')
    }
    .height('100%')
  }
}

@Builder 方法赋值给变量在UI语法中使用

自定义组件Index使用ForEach进行不同@Builder函数的渲染,可以使用builderArr声明的wrapBuilder数组来实现不同的@Builder函数效果。整体代码会更加整洁。

复制代码
@Builder
function MyBuilder(value: string, size: number) {
  Text(value)
    .fontSize(size)
}


@Builder
function YourBuilder(value: string, size: number) {
  Text(value)
    .fontSize(size)
    .fontColor(Color.Pink)
}


const builderArr: WrappedBuilder<[string, number]>[] = [wrapBuilder(MyBuilder), wrapBuilder(YourBuilder)];




@Entry
@Component
struct Index {
  @Builder
  testBuilder() {
    ForEach(builderArr, (item: WrappedBuilder<[string, number]>) => {
      item.builder('Hello World', 30)
    }


    )
  }


  build() {
    Row() {
      Column() {
        this.testBuilder()
      }
      .width('100%')
    }
    .height('100%')
  }
}

引用传递

按引用传递参数时,传递的状态变量的改变会引起@Builder方法内的UI刷新。

复制代码
class Tmp {
  paramA2: string = 'hello';
}


@Builder
function overBuilder(param: Tmp) {
  Column() {
    Text(`wrapBuildervalue:${param.paramA2}`)
  }
}


const wBuilder: WrappedBuilder<[Tmp]> = wrapBuilder(overBuilder);


@Entry
@Component
struct Parent {
  @State label: Tmp = new Tmp();


  build() {
    Column() {
      wBuilder.builder({ paramA2: this.label.paramA2 })
      Button('Click me').onClick(() => {
        this.label.paramA2 = 'ArkUI';
      })
    }
  }
}

常见问题

(1)重复定义wrapBuilder失效

在同一个自定义组件内,同一个wrapBuilder只能初始化一次。示例中,builderObj通过wrapBuilder(MyBuilderFirst)初始化定义后,再次对builderObj赋值wrapBuilder(MyBuilderSecond)不会生效。

复制代码
@Builder
function MyBuilderFirst(value: string, size: number) {
  Text('MyBuilderFirst:' + value)
    .fontSize(size)
}


@Builder
function MyBuilderSecond(value: string, size: number) {
  Text('MyBuilderSecond:' + value)
    .fontSize(size)
}


interface BuilderModel {
  globalBuilder: WrappedBuilder<[string, number]>;
}


@Entry
@Component
struct Index {
  @State message: string = 'Hello World';
  @State builderObj: BuilderModel = { globalBuilder: wrapBuilder(MyBuilderFirst) };


  aboutToAppear(): void {
    setTimeout(() => {
      // wrapBuilder(MyBuilderSecond) 不会生效
      this.builderObj.globalBuilder = wrapBuilder(MyBuilderSecond);
    }, 1000);
  }


  build() {
    Row() {
      Column() {
        this.builderObj.globalBuilder.builder(this.message, 20)
      }
      .width('100%')
    }
    .height('100%')
  }
}