@Builder Macro: Custom Build Functions
Note:
Currently in the beta phase.
The Cangjie UI provides a lightweight UI element reuse mechanism called @Builder, which has a fixed internal UI structure and only interacts with the caller through data transfer. Developers can abstract reusable UI elements into a method and call it within the build method.
For simplicity, functions decorated with @Builder are also referred to as "custom build functions."
Before reading this document, it is recommended to review the following:
Macro Usage Instructions
The @Builder macro can be used in two ways: Private Custom Build Functions defined within a custom component and Global Custom Build Functions defined globally.
Private Custom Build Functions
Example:
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Entry
@Component
class EntryView {
@Builder
func showTextBuilder() {
Text("Hello World")
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Builder
func showTextValueBuilder(param: String) {
Text(param)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
func build() {
Column {
// No parameters
this.showTextBuilder()
// With parameters
this.showTextValueBuilder("Hello @Builder")
}
}
}
Usage:
this.showTextBuilder()
-
Allows defining one or more @Builder methods within a custom component. These methods are considered private, special-type member functions of the component.
-
Private custom build functions can be called within the custom component, the build method, and other custom build functions.
-
Within the custom function body,
thisrefers to the current component. The component's state variables can be accessed within the custom build function. It is recommended to access the component's state variables viathisrather than passing them as parameters.
Global Custom Build Functions
Example:
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Builder
func showTextBuilder() {
Text("Hello World")
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Entry
@Component
class EntryView {
func build() {
Column {
showTextBuilder()
}
}
}
Usage:
showTextBuilder()
-
If component state changes are not involved, it is recommended to use global custom build methods.
-
Global custom build functions can be called within the build method and other custom build functions.
Parameter Passing Rules
Custom build functions support two parameter passing methods: Pass by Value and Pass by Reference. Both must adhere to the following rules:
-
The parameter type must match the declared parameter type.
-
Within the @Builder-decorated function, modifying parameter values is not allowed.
-
The UI syntax within @Builder follows the UI Syntax Rules.
-
Only when a single parameter is passed and the parameter requires direct object literal passing will it be passed by reference. All other passing methods are by value.
Pass by Value Parameters
By default, parameters are passed by value when calling @Builder-decorated functions. When passing state variables, changes to the state variables will not trigger UI refreshes within the @Builder method. Therefore, when using state variables, it is recommended to use Pass by Reference.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Builder
func overBuilder(paramA1: String) {
Row {
Text("UseStateVarByValue: ${paramA1}")
}
}
@Entry
@Component
class EntryView {
@State var label: String = "Hello"
func build() {
Column {
overBuilder(label)
}
}
}
Pass by Reference Parameters
When passing parameters by reference, the parameters can be state variables, and changes to the state variables will trigger UI refreshes within the @Builder method.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class Tmp {
@Publish
var paramA1: String = ""
}
@Builder
func overBuilder(params: Tmp) {
Row {
Text("UseStateVarByReference: ${params.paramA1}")
}
}
@Entry
@Component
class EntryView {
@State var tmp: Tmp = Tmp(paramA1: "Hello")
func build() {
Column {
// When calling the overBuilder component in the parent component,
// pass the parameter to the overBuilder component by reference.
overBuilder(tmp)
Button("Click me")
.onClick({
_ =>
// After clicking "Click me," the UI text changes to "ArkUI."
this.tmp.paramA1 = "ArkUI"
})
}
}
}
Limitations
@Builder triggers dynamic UI rendering only when parameters are passed by reference. Refer to Pass by Reference Parameters.
Usage Scenarios
Using Custom Build Functions Within Custom Components
Create a private @Builder method and call it within a Column using this.builder(). Use the aboutToAppear lifecycle function and a button click event to change the content of builder_value, enabling dynamic UI rendering.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Entry
@Component
class EntryView {
@State var builder_value: String = "Hello"
@Builder
func builder() {
Column {
Text(this.builder_value)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
protected override func aboutToAppear() {
this.builder_value = "Hello World"
}
func build() {
Row {
Column {
Text(this.builder_value)
.fontSize(30)
.fontWeight(FontWeight.Bold)
this.builder()
Button("Click to change builder_value content")
.onClick({
e =>
this.builder_value = "builder_value was clicked"
})
}
}
}
}
Using Global Custom Build Functions
Create a global @Builder method and call it within a Column using overBuilder(). Pass parameters as object literals. Changes to values, whether simple or complex, will trigger UI refreshes.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
import std.collection.ArrayList
@Observed
class ChildTmp {
@Publish
var val: Int64 = 1
}
@Observed
class Tmp {
@Publish
var tmp_value: ChildTmp = ChildTmp()
@Publish
var str_value: String = "Hello"
@Publish
var num_value: Int64 = 0
@Publish
var arrayTmp_value: ObservedArrayList<ChildTmp> = ObservedArrayList<ChildTmp>()
}
@Builder
func overBuilder(param: Tmp) {
Column {
Text("str_value: ${param.str_value}")
Text("num_value: ${param.num_value}")
Text("tmp_value: ${param.tmp_value.val}")
ForEach(
param.arrayTmp_value,
itemGeneratorFunc: {
item: ChildTmp, idx: Int64 => Text("arrayTmp_value: ${item.val}")
}
)
}
}
@Entry
@Component
class EntryView {
@State
var objParam: Tmp = Tmp()
func build() {
Column {
Text("Render UI by calling @Builder").fontSize(20)
overBuilder(this.objParam)
Line()
.width(100.percent)
.height(10)
.backgroundColor(0x000000)
.margin(10)
Button("Click to change parameter values").onClick(
{
_ =>
this.objParam.str_value = "Hello World"
this.objParam.num_value = 1
this.objParam.tmp_value.val = 8
let child_value: ChildTmp = ChildTmp(val: 2)
this.objParam.arrayTmp_value.append(child_value)
}
)
}
}
}
Modifying Macro-Decorated Variables to Trigger UI Refresh
In this scenario, @Builder is only used to display the Text component and does not participate in dynamic UI refresh functionality. Changes to the values in the Text component are triggered by the macro's feature of listening to value changes, not by @Builder's capability.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class Tmp {
@Publish var str_value: String = "Hello"
}
@Entry
@Component
class EntryView {
@State var objParam: Tmp = Tmp()
@State var label: String = "World"
@Builder
func privateBuilder() {
Column {
Text("wrapBuilder str_value: ${this.objParam.str_value}")
Text("wrapBuilder num: ${this.label}")
}
}
func build() {
Column {
Text("Render UI by calling @Builder")
.fontSize(20)
this.privateBuilder()
Line()
.width(100.percent)
.height(10)
.backgroundColor(0x000000)
.margin(10)
Button("Click to change parameter values")
.onClick({
_ =>
this.objParam.str_value = "str_value: Hello World"
this.label = "label Hello World"
})
}
}
}
Using Global and Local @Builder with CustomBuilder Type
When a parameter type is CustomBuilder, the defined @Builder function can be passed in because CustomBuilder is essentially a Function(() -> Unit) type, and @Builder is also a Function type. In this scenario, passing @Builder achieves specific effects.
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Builder
func myBuilder2() {
Column {
Text("Global Builder")
}
.width(100.percent)
.height(100.percent)
.align(Alignment.Center)
}
@Entry
@Component
class EntryView {
@State var isShow: Bool = false
@State var isShow2: Bool = false
@Builder
func myBuilder() {
Column {
Text("Local Builder")
}
.width(100.percent)
.height(100.percent)
.align(Alignment.Center)
}
func build() {
Column {
Button("Local Builder")
.onClick({
e => this.isShow = true
})
.fontSize(20)
.margin(10)
.bindSheet(this.isShow, myBuilder, options: SheetOptions(onDisappear: {=> this.isShow = false}) )
Button("Global Builder")
.onClick({
e => this.isShow2 = true
})
.fontSize(20)
.margin(10)
.bindSheet(this.isShow2, myBuilder2, options: SheetOptions(onDisappear: {=> this.isShow2 = false}) )
}
.justifyContent(FlexAlign.Center)
.backgroundColor(Color.White)
.width(100.percent)
.height(100.percent)
}
}
```### Multi-layer @Builder Method Nesting
Calling custom components or other @Builder methods within an @Builder method enables scenarios of nested @Builder usage. To achieve dynamic UI refresh functionality for the innermost @Builder, it is essential to ensure that each layer of @Builder invocation uses pass-by-reference.
<!-- run -->
```cangjie
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Observed
class Tmp {
@Publish var paramA1: String = ""
}
@Builder
func parentBuilder(params: Tmp) {
Row {
Text("parentBuilder: ${params.paramA1}")
}
childBuilder(params)
}
@Builder
func childBuilder(params: Tmp) {
Row {
Text("childBuilder: ${params.paramA1}")
}
grandsonBuilder(params)
}
@Builder
func grandsonBuilder(params: Tmp) {
Row {
Text("grandsonBuilder: ${params.paramA1}")
}
}
@Entry
@Component
class EntryView {
@State var tmp: Tmp = Tmp(paramA1: "Hello")
func build() {
Column {
parentBuilder(this.tmp)
Text(this.tmp.paramA1)
Button("Click me")
.onClick({
_ =>
this.tmp.paramA1 = "ArkUI"
})
}
}
}