1# \@Builder装饰器:自定义构建函数
2
3ArkUI提供了一种轻量的UI元素复用机制\@Builder,该自定义组件内部UI结构固定,仅与使用方进行数据传递,开发者可以将重复使用的UI元素抽象成一个方法,在build方法里调用。
4
5为了简化语言,我们将\@Builder装饰的函数也称为“自定义构建函数”。
6
7
8> **说明:**
9>
10> 从API version 9开始,该装饰器支持在ArkTS卡片中使用。
11>
12> 从API version 11开始,该装饰器支持在原子化服务中使用。
13
14## 限制条件
15
16- \@Builder通过按引用传递的方式传入参数,才会触发动态渲染UI,并且参数只能是一个。
17
18- \@Builder如果传入的参数是两个或两个以上,不会触发动态渲染UI。
19
20- \@Builder传入的参数中同时包含按值传递和按引用传递两种方式,不会触发动态渲染UI。
21
22- \@Builder的参数必须按照对象字面量的形式,把所需要的属性一一传入,才会触发动态渲染UI。
23
24## 装饰器使用说明
25
26### 私有自定义构建函数
27
28定义的语法:
29
30```ts
31@Builder MyBuilderFunction() {}
32```
33
34使用方法:
35
36```ts
37this.MyBuilderFunction()
38```
39
40- 允许在自定义组件内定义一个或多个@Builder方法,该方法被认为是该组件的私有、特殊类型的成员函数。
41
42- 私有自定义构建函数允许在自定义组件内、build方法和其他自定义构建函数中调用。
43
44- 在自定义函数体中,this指代当前所属组件,组件的状态变量可以在自定义构建函数内访问。建议通过this访问自定义组件的状态变量而不是参数传递。
45
46
47### 全局自定义构建函数
48
49定义的语法:
50
51```ts
52@Builder function MyGlobalBuilderFunction() { ... }
53```
54
55使用方法:
56
57```ts
58MyGlobalBuilderFunction()
59```
60
61- 如果不涉及组件状态变化,建议使用全局的自定义构建方法。
62
63- 全局自定义构建函数允许在build方法和其他自定义构建函数中调用。
64
65
66## 参数传递规则
67
68自定义构建函数的参数传递有[按值传递](#按值传递参数)和[按引用传递](#按引用传递参数)两种,均需遵守以下规则:
69
70- 参数的类型必须与参数声明的类型一致,不允许undefined、null和返回undefined、null的表达式。
71
72- 在@Builder修饰的函数内部,不允许改变参数值。
73
74- \@Builder内UI语法遵循[UI语法规则](arkts-create-custom-components.md#build函数)。
75
76- 只有传入一个参数,且参数需要直接传入对象字面量才会按引用传递该参数,其余传递方式均为按值传递。
77
78
79### 按引用传递参数
80
81按引用传递参数时,传递的参数可为状态变量,且状态变量的改变会引起\@Builder方法内的UI刷新。
82
83```ts
84class Tmp {
85  paramA1: string = ''
86}
87
88@Builder function overBuilder(params: Tmp) {
89  Row() {
90    Text(`UseStateVarByReference: ${params.paramA1} `)
91  }
92}
93@Entry
94@Component
95struct Parent {
96  @State label: string = 'Hello';
97  build() {
98    Column() {
99      // Pass the this.label reference to the overBuilder component when the overBuilder component is called in the Parent component.
100      overBuilder({ paramA1: this.label })
101      Button('Click me').onClick(() => {
102        // After Click me is clicked, the UI text changes from Hello to ArkUI.
103        this.label = 'ArkUI';
104      })
105    }
106  }
107}
108```
109
110按引用传递参数时,如果在\@Builder方法内调用自定义组件,ArkUI提供[$$](arkts-two-way-sync.md)作为按引用传递参数的范式。
111
112```ts
113class Tmp {
114  paramA1: string = ''
115}
116
117@Builder function overBuilder($$: Tmp) {
118  Row() {
119    Column() {
120      Text(`overBuilder===${$$.paramA1}`)
121      HelloComponent({message: $$.paramA1})
122    }
123  }
124}
125
126@Component
127struct HelloComponent {
128  @Prop message: string;
129
130  build() {
131    Row() {
132      Text(`HelloComponent===${this.message}`)
133    }
134  }
135}
136
137@Entry
138@Component
139struct Parent {
140  @State label: string = 'Hello';
141  build() {
142    Column() {
143      // Pass the this.label reference to the overBuilder component when the overBuilder component is called in the Parent component.
144      overBuilder({paramA1: this.label})
145      Button('Click me').onClick(() => {
146        // After Click me is clicked, the UI text changes from Hello to ArkUI.
147        this.label = 'ArkUI';
148      })
149    }
150  }
151}
152```
153
154### 按值传递参数
155
156调用\@Builder装饰的函数默认按值传递。当传递的参数为状态变量时,状态变量的改变不会引起\@Builder方法内的UI刷新。所以当使用状态变量的时候,推荐使用[按引用传递](#按引用传递参数)。
157
158```ts
159@Builder function overBuilder(paramA1: string) {
160  Row() {
161    Text(`UseStateVarByValue: ${paramA1} `)
162  }
163}
164@Entry
165@Component
166struct Parent {
167  @State label: string = 'Hello';
168  build() {
169    Column() {
170      overBuilder(this.label)
171    }
172  }
173}
174```
175
176使用按值传递的方式,在@ComponentV2装饰器修饰的自定义组件里配合使用@ObservedV2和@Trace装饰器可以实现刷新UI功能。
177
178【正例】
179
180在@ComponentV2装饰中,只有使用@ObservedV2修饰的ParamTmp类和@Trace修饰的count属性才可以触发UI的刷新。
181
182```ts
183@ObservedV2
184class ParamTmp {
185  @Trace count : number = 0;
186}
187
188@Builder
189function renderText(param: ParamTmp) {
190  Column() {
191    Text(`param : ${param.count}`)
192      .fontSize(20)
193      .fontWeight(FontWeight.Bold)
194  }
195}
196
197@Builder
198function renderMap(paramMap: Map<string,number>) {
199  Text(`paramMap : ${paramMap.get('name')}`)
200    .fontSize(20)
201    .fontWeight(FontWeight.Bold)
202}
203
204@Builder
205function renderSet(paramSet: Set<number>) {
206  Text(`paramSet : ${paramSet.size}`)
207    .fontSize(20)
208    .fontWeight(FontWeight.Bold)
209}
210
211@Builder
212function renderNumberArr(paramNumArr: number[]) {
213  Text(`paramNumArr : ${paramNumArr[0]}`)
214    .fontSize(20)
215    .fontWeight(FontWeight.Bold)
216}
217
218@Entry
219@ComponentV2
220struct PageBuilder {
221  @Local builderParams: ParamTmp = new ParamTmp();
222  @Local map_value: Map<string,number> = new Map();
223  @Local set_value: Set<number> = new Set([0]);
224  @Local numArr_value: number[] = [0];
225  private progressTimer: number = -1;
226
227  aboutToAppear(): void {
228    this.progressTimer = setInterval(() => {
229      if (this.builderParams.count < 100) {
230        this.builderParams.count += 5;
231        this.map_value.set('name', this.builderParams.count);
232        this.set_value.add(this.builderParams.count);
233        this.numArr_value[0] = this.builderParams.count;
234      } else {
235        clearInterval(this.progressTimer)
236      }
237    }, 500);
238  }
239
240  @Builder
241  localBuilder() {
242    Column() {
243      Text(`localBuilder : ${this.builderParams.count}`)
244        .fontSize(20)
245        .fontWeight(FontWeight.Bold)
246    }
247  }
248
249  build() {
250    Column() {
251      this.localBuilder()
252      Text(`builderParams :${this.builderParams.count}`)
253        .fontSize(20)
254        .fontWeight(FontWeight.Bold)
255      renderText(this.builderParams)
256      renderText({ count: this.builderParams.count })
257      renderMap(this.map_value)
258      renderSet(this.set_value)
259      renderNumberArr(this.numArr_value)
260    }
261    .width('100%')
262    .height('100%')
263  }
264}
265```
266
267【反例】
268
269在@ComponentV2装饰的自定义组件中,使用简单数据类型不可以触发UI的刷新。
270
271```ts
272@ObservedV2
273class ParamTmp {
274  @Trace count : number = 0;
275}
276
277@Builder
278function renderNumber(paramNum: number) {
279  Text(`paramNum : ${paramNum}`)
280    .fontSize(30)
281    .fontWeight(FontWeight.Bold)
282}
283
284@Entry
285@ComponentV2
286struct PageBuilder {
287  @Local class_value: ParamTmp = new ParamTmp();
288  // 此处使用简单数据类型不支持刷新UI的能力。
289  @Local num_value: number = 0;
290  private progressTimer: number = -1;
291
292  aboutToAppear(): void {
293    this.progressTimer = setInterval(() => {
294      if (this.class_value.count < 100) {
295        this.class_value.count += 5;
296        this.num_value += 5;
297      } else {
298        clearInterval(this.progressTimer)
299      }
300    }, 500);
301  }
302
303  build() {
304    Column() {
305      renderNumber(this.num_value)
306    }
307    .width('100%')
308    .height('100%')
309    .padding(50)
310  }
311}
312```
313
314## 使用场景
315
316### 自定义组件内使用自定义构建函数
317
318创建私有的\@Builder方法,在Column里面使用this.builder()方式调用,通过aboutToAppear生命周期函数和按钮的点击事件改变builder_value的内容,实现动态渲染UI。
319
320```ts
321@Entry
322@Component
323struct PrivateBuilder {
324  @State builder_value: string = 'Hello';
325
326  @Builder builder() {
327    Column(){
328      Text(this.builder_value)
329        .fontSize(30)
330        .fontWeight(FontWeight.Bold)
331    }
332  }
333
334  aboutToAppear(): void {
335    setTimeout(() => {
336      this.builder_value = 'Hello World';
337    },3000)
338  }
339
340  build() {
341    Row() {
342      Column() {
343        Text(this.builder_value)
344          .fontSize(30)
345          .fontWeight(FontWeight.Bold)
346        this.builder()
347        Button('点击改变builder_value内容')
348          .onClick(() => {
349            this.builder_value ='builder_value被点击了'
350          })
351      }
352    }
353  }
354}
355```
356
357### 使用全局自定义构建函数
358
359创建全局的\@Builder方法,在Column里面使用overBuilder()方式调用,通过以对象字面量的形式传递参数,无论是简单类型还是复杂类型,值的改变都会引起UI界面的刷新。
360
361```ts
362class ChildTmp {
363  val: number = 1;
364}
365
366class Tmp {
367  str_value: string = 'Hello';
368  num_value: number = 0;
369  tmp_value: ChildTmp = new ChildTmp();
370  arrayTmp_value: Array<ChildTmp> = [];
371}
372
373@Builder function overBuilder(param: Tmp) {
374  Column() {
375    Text(`str_value: ${param.str_value}`)
376    Text(`num_value: ${param.num_value}`)
377    Text(`tmp_value: ${param.tmp_value.val}`)
378    ForEach(param.arrayTmp_value, (item: ChildTmp) => {
379      Text(`arrayTmp_value: ${item.val}`)
380    }, (item: ChildTmp) => JSON.stringify(item))
381  }
382}
383
384@Entry
385@Component
386struct Parent {
387  @State objParam: Tmp = new Tmp();
388  build() {
389    Column() {
390      Text('通过调用@Builder渲染UI界面')
391        .fontSize(20)
392      overBuilder({str_value: this.objParam.str_value, num_value: this.objParam.num_value, tmp_value: this.objParam.tmp_value, arrayTmp_value: this.objParam.arrayTmp_value})
393      Line()
394        .width('100%')
395        .height(10)
396        .backgroundColor('#000000').margin(10)
397      Button('点击改变参数值').onClick(() => {
398        this.objParam.str_value = 'Hello World';
399        this.objParam.num_value = 1;
400        this.objParam.tmp_value.val = 8;
401        const child_value: ChildTmp = {
402          val: 2
403        }
404        this.objParam.arrayTmp_value.push(child_value)
405      })
406    }
407  }
408}
409```
410
411### 修改装饰器修饰的变量触发UI刷新
412
413此种方式是使用了装饰器的特性,监听值的改变触发UI刷新,不通过\@Builder传递参数。
414
415```ts
416class Tmp {
417  str_value: string = 'Hello';
418}
419
420@Entry
421@Component
422struct Parent {
423  @State objParam: Tmp = new Tmp();
424  @State label: string = 'World';
425
426  @Builder privateBuilder() {
427    Column() {
428      Text(`wrapBuilder str_value: ${this.objParam.str_value}`)
429      Text(`wrapBuilder num: ${this.label}`)
430    }
431  }
432
433  build() {
434    Column() {
435      Text('通过调用@Builder渲染UI界面')
436        .fontSize(20)
437      this.privateBuilder()
438      Line()
439        .width('100%')
440        .height(10)
441        .backgroundColor('#000000').margin(10)
442      Button('点击改变参数值').onClick(() => {
443        this.objParam.str_value = 'str_value Hello World';
444        this.label = 'label Hello World'
445      })
446    }
447  }
448}
449```
450
451### 使用全局和局部的@Builder传入customBuilder类型
452
453```ts
454@Builder
455function overBuilder() {
456  Row() {
457    Text('全局 Builder')
458      .fontSize(30)
459      .fontWeight(FontWeight.Bold)
460  }
461}
462
463@Entry
464@Component
465struct customBuilderDemo {
466  @State arr: number[] = [0, 1, 2, 3, 4];
467
468  @Builder privateBuilder() {
469    Row() {
470      Text('局部 Builder')
471        .fontSize(30)
472        .fontWeight(FontWeight.Bold)
473    }
474  }
475
476  build() {
477    Column() {
478      List({ space: 10 }) {
479        ForEach(this.arr, (item: number) => {
480          ListItem(){
481            Text(`${item}`)
482              .width('100%')
483              .height(100)
484              .fontSize(16)
485              .textAlign(TextAlign.Center)
486              .borderRadius(10)
487              .backgroundColor(0xFFFFFF)
488          }
489            .swipeAction({
490              start: {
491                builder: overBuilder()
492              },
493              end: {
494                builder: () => { this.privateBuilder() }
495              }
496            })
497        }, (item: string) => JSON.stringify(item))
498      }
499    }
500  }
501}
502```
503
504### 多层\@Builder方法嵌套使用
505
506在\@Builder方法内调用自定义组件或者其他\@Builder方法,ArkUI提供[$$](arkts-two-way-sync.md)作为按引用传递参数的范式。
507
508```ts
509class Tmp {
510  paramA1: string = '';
511}
512
513@Builder function parentBuilder($$: Tmp) {
514  Row() {
515    Column() {
516      Text(`parentBuilder===${$$.paramA1}`)
517        .fontSize(30)
518        .fontWeight(FontWeight.Bold)
519      HelloComponent({message: $$.paramA1})
520      childBuilder({paramA1: $$.paramA1})
521    }
522  }
523}
524
525@Component
526struct HelloComponent {
527  @Prop message: string = '';
528
529  build() {
530    Row() {
531      Text(`HelloComponent===${this.message}`)
532        .fontSize(30)
533        .fontWeight(FontWeight.Bold)
534    }
535  }
536}
537
538@Builder
539function childBuilder($$: Tmp) {
540  Row() {
541    Column() {
542      Text(`childBuilder===${$$.paramA1}`)
543        .fontSize(30)
544        .fontWeight(FontWeight.Bold)
545      HelloChildComponent({message: $$.paramA1})
546      grandsonBuilder({paramA1: $$.paramA1})
547    }
548  }
549}
550
551@Component
552struct HelloChildComponent {
553  @State message: string = '';
554  build() {
555    Row() {
556      Text(`HelloChildComponent===${this.message}`)
557        .fontSize(30)
558        .fontWeight(FontWeight.Bold)
559    }
560  }
561}
562
563@Builder function grandsonBuilder($$: Tmp) {
564  Row() {
565    Column() {
566      Text(`grandsonBuilder===${$$.paramA1}`)
567        .fontSize(30)
568        .fontWeight(FontWeight.Bold)
569      HelloGrandsonComponent({message: $$.paramA1})
570    }
571  }
572}
573
574@Component
575struct HelloGrandsonComponent {
576  @Prop message: string;
577  build() {
578    Row() {
579      Text(`HelloGrandsonComponent===${this.message}`)
580        .fontSize(30)
581        .fontWeight(FontWeight.Bold)
582    }
583  }
584}
585
586@Entry
587@Component
588struct Parent {
589  @State label: string = 'Hello';
590  build() {
591    Column() {
592      parentBuilder({paramA1: this.label})
593      Button('Click me').onClick(() => {
594        this.label = 'ArkUI';
595      })
596    }
597  }
598}
599```
600
601### \@Builder函数联合V2装饰器使用
602
603使用全局@Builder和局部@Builder在@ComponentV2修饰的自定义组件中调用,修改相关变量触发UI刷新。
604
605```ts
606@ObservedV2
607class Info {
608  @Trace name: string = '';
609  @Trace age: number = 0;
610}
611
612@Builder
613function overBuilder(param: Info) {
614  Column() {
615    Text(`全局@Builder name :${param.name}`)
616      .fontSize(30)
617      .fontWeight(FontWeight.Bold)
618    Text(`全局@Builder age :${param.age}`)
619      .fontSize(30)
620      .fontWeight(FontWeight.Bold)
621  }
622}
623
624@ComponentV2
625struct ChildPage {
626  @Require @Param childInfo: Info;
627  build() {
628    overBuilder({name: this.childInfo.name, age: this.childInfo.age})
629  }
630}
631
632@Entry
633@ComponentV2
634struct ParentPage {
635  info1: Info = { name: "Tom", age: 25 };
636  @Local info2: Info = { name: "Tom", age: 25 };
637
638  @Builder
639  privateBuilder() {
640    Column() {
641      Text(`局部@Builder name :${this.info1.name}`)
642        .fontSize(30)
643        .fontWeight(FontWeight.Bold)
644      Text(`局部@Builder age :${this.info1.age}`)
645        .fontSize(30)
646        .fontWeight(FontWeight.Bold)
647    }
648  }
649
650  build() {
651    Column() {
652      Text(`info1: ${this.info1.name}  ${this.info1.age}`) // Text1
653        .fontSize(30)
654        .fontWeight(FontWeight.Bold)
655      this.privateBuilder() // 调用局部@Builder
656      Line()
657        .width('100%')
658        .height(10)
659        .backgroundColor('#000000').margin(10)
660      Text(`info2: ${this.info2.name}  ${this.info2.age}`) // Text2
661        .fontSize(30)
662        .fontWeight(FontWeight.Bold)
663      overBuilder({ name: this.info2.name, age: this.info2.age}) // 调用全局@Builder
664      Line()
665        .width('100%')
666        .height(10)
667        .backgroundColor('#000000').margin(10)
668      Text(`info1: ${this.info1.name}  ${this.info1.age}`) // Text1
669        .fontSize(30)
670        .fontWeight(FontWeight.Bold)
671      ChildPage({ childInfo: this.info1}) // 调用自定义组件
672      Line()
673        .width('100%')
674        .height(10)
675        .backgroundColor('#000000').margin(10)
676      Text(`info2: ${this.info2.name}  ${this.info2.age}`) // Text2
677        .fontSize(30)
678        .fontWeight(FontWeight.Bold)
679      ChildPage({ childInfo: this.info2}) // 调用自定义组件
680      Line()
681        .width('100%')
682        .height(10)
683        .backgroundColor('#000000').margin(10)
684      Button("change info1&info2")
685        .onClick(() => {
686          this.info1 = { name: "Cat", age: 18} // Text1不会刷新,原因是没有装饰器修饰监听不到值的改变。
687          this.info2 = { name: "Cat", age: 18} // Text2会刷新,原因是有装饰器修饰,可以监听到值的改变。
688        })
689    }
690  }
691}
692```
693
694## 常见问题
695
696### \@Builder存在两个或者两个以上参数
697
698当参数存在两个或者两个以上的时候,就算通过对象字面量的形式传递,值的改变也不会引起UI刷新。
699
700【反例】
701
702```ts
703class GlobalTmp {
704  str_value: string = 'Hello';
705}
706
707@Builder function overBuilder(param: GlobalTmp, num: number) {
708  Column() {
709    Text(`str_value: ${param.str_value}`)
710    Text(`num: ${num}`)
711  }
712}
713
714@Entry
715@Component
716struct Parent {
717  @State objParam: GlobalTmp = new GlobalTmp();
718  @State num: number = 0;
719  build() {
720    Column() {
721      Text('通过调用@Builder渲染UI界面')
722        .fontSize(20)
723      overBuilder({str_value: this.objParam.str_value}, this.num) // 此处出现问题,使用了两个参数。
724      Line()
725        .width('100%')
726        .height(10)
727        .backgroundColor('#000000').margin(10)
728      Button('点击改变参数值').onClick(() => {
729        this.objParam.str_value = 'Hello World';
730        this.num = 1;
731      })
732    }
733  }
734}
735```
736
737【反例】
738
739```ts
740class GlobalTmp {
741  str_value: string = 'Hello';
742}
743class SecondTmp {
744  num_value: number = 0;
745}
746@Builder function overBuilder(param: GlobalTmp, num: SecondTmp) {
747  Column() {
748    Text(`str_value: ${param.str_value}`)
749    Text(`num: ${num.num_value}`)
750  }
751}
752
753@Entry
754@Component
755struct Parent {
756  @State strParam: GlobalTmp = new GlobalTmp();
757  @State numParam: SecondTmp = new SecondTmp();
758  build() {
759    Column() {
760      Text('通过调用@Builder渲染UI界面')
761        .fontSize(20)
762      overBuilder({str_value: this.strParam.str_value}, {num_value: this.numParam.num_value}) // 此处出现问题,使用了两个参数。
763      Line()
764        .width('100%')
765        .height(10)
766        .backgroundColor('#000000').margin(10)
767      Button('点击改变参数值').onClick(() => {
768        this.strParam.str_value = 'Hello World';
769        this.numParam.num_value = 1;
770      })
771    }
772  }
773}
774```
775
776\@Builder只接受一个参数,当传入一个参数的时候,通过对象字面量的形式传递,值的改变会引起UI的刷新。
777
778【正例】
779
780```ts
781class GlobalTmp {
782  str_value: string = 'Hello';
783  num_value: number = 0;
784}
785@Builder function overBuilder(param: GlobalTmp) {
786  Column() {
787    Text(`str_value: ${param.str_value}`)
788    Text(`num: ${param.num_value}`)
789  }
790}
791
792@Entry
793@Component
794struct Parent {
795  @State objParam: GlobalTmp = new GlobalTmp();
796  build() {
797    Column() {
798      Text('通过调用@Builder渲染UI界面')
799        .fontSize(20)
800      overBuilder({str_value: this.objParam.str_value, num_value: this.objParam.num_value})
801      Line()
802        .width('100%')
803        .height(10)
804        .backgroundColor('#000000').margin(10)
805      Button('点击改变参数值').onClick(() => {
806        this.objParam.str_value = 'Hello World';
807        this.objParam.num_value = 1;
808      })
809    }
810  }
811}
812```
813
814### \@Builder函数里面使用的组件没有根节点包裹
815
816在\@Builder函数里使用if判断语句时,创建的组件没有被Column/Row(根节点)包裹,会出现组件创建不出来的情况。
817
818【反例】
819
820```ts
821const showComponent: boolean = true;
822@Builder function OverlayNode() {
823  // 没有Column或者Row根节点导致Text组件没有创建
824  if (showComponent) {
825      Text("This is overlayNode Blue page")
826        .fontSize(20)
827        .fontColor(Color.Blue)
828        .height(100)
829        .textAlign(TextAlign.End)
830    } else {
831      Text("This is overlayNode Red page")
832        .fontSize(20)
833        .fontColor(Color.Red)
834    }
835}
836
837@Entry
838@Component
839struct OverlayExample {
840
841  build() {
842    RelativeContainer() {
843      Text('Hello World')
844        .overlay(OverlayNode(), { align: Alignment.Center})
845    }
846    .height('100%')
847    .width('100%')
848  }
849}
850```
851
852【正例】
853
854```ts
855const showComponent: boolean = true;
856@Builder function OverlayNode() {
857  Column() {
858    if (showComponent) {
859      Text("This is overlayNode Blue page")
860        .fontSize(20)
861        .fontColor(Color.Blue)
862        .height(100)
863        .textAlign(TextAlign.End)
864    } else {
865      Text("This is overlayNode Red page")
866        .fontSize(20)
867        .fontColor(Color.Red)
868    }
869  }
870}
871
872@Entry
873@Component
874struct OverlayExample {
875
876  build() {
877    RelativeContainer() {
878      Text('Hello World')
879        .overlay(OverlayNode(), { align: Alignment.Center})
880    }
881    .height('100%')
882    .width('100%')
883  }
884}
885```