@Builder Decorator: Custom Builder Function
ArkUI provides a lightweight UI element reuse mechanism @Builder. The internal UI structure of ArkUI is fixed and only data is transferred with the user. Developers can abstract UI elements that are repeatedly used as functions and call them in the build function.
Functions decorated by @Builder are also called custom build functions.
Before reading this topic, you are advised to read Basic Syntax Overview, Declarative UI Description, and Creating a Custom Component.
The differences between the @Builder decorator and @Component decorator in functions and usage are as follows:
-
The @Builder decorator is used to encapsulate reusable UI structures and extract repeated layout code to improve development efficiency. It is strictly prohibited to define state variable or use custom-component-lifecycle in the decorator. Data interaction must be completed through parameter transfer or access to the state variable of the component to which the decorator belongs.
-
In the ArkUI framework, the @Component decorator is the core mechanism for encapsulating complex UI components. It allows developers to combine multiple basic components to build a reusable composite UI. This decorator not only supports the definition of internal state variables, but also manages the lifecycle of components.
NOTE
The initial APIs of this module are supported since API version 7.
This decorator can be used in ArkTS widgets since API version 9.
This decorator can be used in atomic services since API version 11.
How to Use
The @Builder decorator can be used in two modes: as a private custom builder function within a custom component, or as a global custom builder function defined at global scope.
Private Custom Builder Function
The following is an example:
@Entry
@Component
struct BuilderDemo {
@Builder
showTextBuilder() {
// @Builder decorates this function so that the Text component can be configured and built in chain mode.
Text('Hello World')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Builder
showTextValueBuilder(param: string) {
Text(param)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
build() {
Column() {
// Without parameter.
this.showTextBuilder()
// With a parameter.
this.showTextValueBuilder('Hello @Builder')
}
}
}
Invocation pattern:
-
One or more @Builder functions can be defined in a custom component. These functions are treated as private and special member functions of the owning component.
-
Private custom builder functions can be called in custom components, build(), and other custom builder functions.
-
In a custom component, this indicates the component to which the component belongs. The status variables of the component can be accessed in the custom builder functions. It is recommended that this be used to access the status variable of the component instead of being transferred through parameters.
Global Custom Builder Function
The following is an example:
// Global custom builder function showTextBuilder.
@Builder
function showTextBuilder() {
Text('Hello World')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Entry
@Component
struct BuilderSample {
build() {
Column() {
showTextBuilder()
}
}
}
-
If the component status variable does not change, you are advised to use the global custom build function.
-
Global custom build functions can be called in the build function and other custom build functions.
Parameter Passing Rules
Parameters for custom builder functions can be passed by callback, by reference, or by value. The following rules must be followed:
-
The parameter type of the function decorated by @Builder cannot be undefined, null, or an expression that returns undefined or null.
-
All parameters must be immutable inside the custom builder function decorated by @Builder.
-
The @Builder function body follows the same syntax rules as the build() function.
-
The UI components in the @Builder function can be updated during callback-based transfer and reference-based transfer. Passing by reference takes effect only when one parameter is passed and the parameter is directly passed to the object literal. If there are multiple parameters, the UI component in the @Builder function cannot be refreshed.
-
When a reference is passed, the attribute of the parameter cannot be modified in the @Builder function. However, when UIUtils.makeBinding is used and the write callback is passed, the attribute can be modified in the @Builder function and synchronized to the component that calls the @Builder function.
Passing Parameters by Callback
From API version 20, you can use the UIUtils.makeBinding() function, the Binding class, and the MutableBinding class to refresh state variables in the @Builder function. For details, see State Variables Can Be Refreshed in the @Builder.
Use UIUtils.makeBinding() to wrap the callback function for reading status variables and transfer the callback function as a parameter to the @Builder function. The UI component in the @Builder function can be refreshed. The callback function of the write status variable transferred in UIUtils.makeBinding() can transfer the parameter changes in @Builder to the component that calls the @Builder function.
import { Binding, MutableBinding, UIUtils } from '@kit.ArkUI';
@Builder
function customButton(num1: Binding<number>, num2: MutableBinding<number>) {
Row() {
Column() {
Text(`number1: ${num1.value}, number2: ${num2.value}`)
Button(`only change number2`)
.onClick(() => {
// Assign a value of the MutableBinding type and transfer the modification to the parent component.
num2.value += 1;
})
}
}
}
@Entry
@ComponentV2
struct ParameterMakeBinding {
@Local number1: number = 5;
@Local number2: number = 12;
build() {
Column() {
customButton(
// Use makeBinding to pass parameters. The read callback needs to be passed. The Binding type is returned. The UI of the component in @Builder can be refreshed.
UIUtils.makeBinding<number>(() => this.number1),
// The MutableBinding type is returned when the write callback is passed to makeBinding. The UI of the component in @Builder can be refreshed and the attribute modification can be synchronized.
UIUtils.makeBinding<number>(
() => this.number2,
(val: number) => {
this.number2 = val;
})
)
}
}
}
By-Reference Parameter Passing
In by-reference parameter passing, state variables can be passed, and the change of these state variables causes the UI re-rendering in the @Builder function.
class Tmp {
public paramA1: string = '';
}
@Builder
function overBuilderByReference(params: Tmp) {
Row() {
Text(`UseStateVarByReference: ${params.paramA1} `)
}
}
@Entry
@Component
struct ParameterReference {
@State label: string = 'Hello';
build() {
Column() {
// When the overBuilderByReference component is called in the parent component:
// Transfer this.label to the overBuilderByReference component by reference.
overBuilderByReference({ paramA1: this.label })
Button('Click me').onClick(() => {
// After you click "Click me", the UI text changes from "Hello" to "ArkUI".
this.label = 'ArkUI';
})
}
}
}
By-Value Parameter Passing
By default, parameters in the @Builder decorated functions are passed by value. If the transferred parameter is a status variable, the change of the status variable does not cause the UI update in the @Builder function. Therefore, when using state variables, you are advised to use Passing Parameters By Callback or Passing Parameters By Reference.
@Builder
function overBuilderByValue(paramA1: string) {
Row() {
Text(`UseStateVarByValue: ${paramA1} `)
}
}
@Entry
@Component
struct ParameterValue {
@State label: string = 'Hello';
build() {
Column() {
// Pass parameters by value. Changes to state variables will not trigger UI refresh inside overBuilderByValue.
overBuilderByValue(this.label)
}
}
}
Constraints
-
@If MutableBinding is not used in a function decorated by Builder, the parameter value cannot be modified. The modification does not trigger UI update. If passing parameters by reference and only one parameter is passed, modifying the internal attributes of the parameter will throw a runtime error. You can use MutableBinding to modify parameter values in the function decorated by @Builder. For details, see Changing the Input Parameters in the @Builder Decorated Function.
-
When an @Builder passes a parameter by reference, dynamic UI rendering can be triggered. Refer to By-Reference Parameter Passing.
-
If an @Builder receives two or more parameters and does not pass parameters by callback, dynamic UI rendering will not be triggered. For details, see Multiple Parameters in @Builder.
-
If the parameters passed to @Builder contain both value passing and reference passing, dynamic UI rendering will not be triggered. For details, see Multiple Parameters in @Builder.
-
Modifying properties of a parameter inside an @Builder function is not allowed; doing so will cause a runtime error. Starting from API version 23, error code 140109 will be returned. For an example, see Changing the Input Parameters in the @Builder Decorated Function.
Use Cases
Using Custom Builder Function in Custom Component
Create a private @Builder function and use this.builder() to call the function in Column. Update builderValue through the aboutToAppear lifecycle function and button click event, implementing dynamic UI rendering.
@Entry
@Component
struct PrivateBuilder {
@State builderValue: string = 'Hello';
@Builder
builder() {
Column() {
Text(this.builderValue)
.width(230)
.height(40)
.backgroundColor('#ffeae5e5')
.borderRadius(20)
.margin(12)
.textAlign(TextAlign.Center)
}
}
aboutToAppear(): void {
setTimeout(() => {
this.builderValue = 'Hello World';
}, 2000);
}
build() {
Row() {
Column() {
Text(this.builderValue)
.width(230)
.height(40)
.backgroundColor('#ffeae5e5')
.borderRadius(20)
.textAlign(TextAlign.Center)
this.builder()
// Click the button to update builderValue to update the text display.
Button('Click to change the builderValue')
.onClick(() => {
this.builderValue = 'builderValue was clicked';
})
}
.height('100%')
.width('100%')
}
}
}

Global Custom Builder Function
Create a global @Builder function and call it in overBuilder() mode in Column. When transferring parameters, you can use the object literal form. Any change of the value will trigger the refresh of the UI, regardless of the simple or complex type.
class ChildTmp {
public val: number = 1;
}
class ParamTmp {
public strValue: string = 'Hello';
public numValue: number = 0;
public tmpValue: ChildTmp = new ChildTmp();
public arrayTmpValue: Array<ChildTmp> = [];
}
@Builder
function overBuilder(param: ParamTmp) {
Column() {
Text(`strValue: ${param.strValue}`)
.width(230)
.height(40)
.margin(12)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
Text(`numValue: ${param.numValue}`)
.width(230)
.height(40)
.margin(12)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
Text(`tmpValue: ${param.tmpValue.val}`)
.width(230)
.height(40)
.margin(12)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
ForEach(param.arrayTmpValue, (item: ChildTmp) => {
ListItem() {
Text(`arrayTmpValue: ${item.val}`)
.width(230)
.height(40)
.margin(12)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
}
}, (item: ChildTmp) => JSON.stringify(item))
}
}
@Entry
@Component
struct ParentDemo {
@State objParam: ParamTmp = new ParamTmp();
build() {
Column() {
Text('UI Rendered via @Builder')
.fontSize(20)
.margin(12)
// Call the global @Builder function overBuilder.
overBuilder({
strValue: this.objParam.strValue,
numValue: this.objParam.numValue,
tmpValue: this.objParam.tmpValue,
arrayTmpValue: this.objParam.arrayTmpValue
})
// Click the button to update objParam and trigger the refresh of components in overBuilder.
Button('Update Values').onClick(() => {
this.objParam.strValue = 'Hello World';
this.objParam.numValue = 1;
this.objParam.tmpValue.val = 8;
const childValue: ChildTmp = {
val: 2
}
this.objParam.arrayTmpValue.push(childValue);
})
}
.height('100%')
.width('100%')
}
}

Changing the Variables Decorated by the Decorator Triggers UI Re-rendering
In this scenario, @Builder defines the Text component layout but does not handle dynamic UI updates. UI re-rendering occurs when decorator-observed values change, not through @Builder's reactive capabilities.
class ChildrenTmp {
public strValue: string = 'Hello';
}
@Entry
@Component
struct ParentSample {
@State objParam: ChildrenTmp = new ChildrenTmp();
@State label: string = 'World';
@Builder
privateBuilder() {
Column() {
Text(`wrapBuilder strValue: ${this.objParam.strValue}`)
.width(350)
.height(40)
.margin(12)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
Text(`wrapBuilder num: ${this.label}`)
.width(350)
.height(40)
.margin(12)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
}
}
build() {
Column() {
Text('UI Rendered via @Builder')
.fontSize(20)
this.privateBuilder()
// Click the button to update label and trigger the refresh of Text components.
Button('Update Values').onClick(() => {
this.objParam.strValue = 'strValue Hello World';
this.label = 'label Hello World';
})
}
.height('100%')
.width('100%')
}
}

Using Functions Decorated with @Builder as CustomBuilder Types
When the parameter type is CustomBuilder, the defined @Builder function can be passed. CustomBuilder is actually of the Function(() => any) or void type, and @Builder is also of the Function type. Therefore, a specific effect can be implemented by transferring @Builder.
When the global @Builder function is transferred as the CustomBuilder type, the this context needs to be bound. You can directly call the global @Builder function. The compilation tool chain automatically generates the code bound to the this context.
@Builder
function overBuilderDemo() {
Row() {
Text('Global Builder')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
@Entry
@Component
struct customBuilderDemo {
@State arr: number[] = [0, 1, 2, 3, 4];
@Builder
privateBuilder() {
Row() {
Text('Private Builder')
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
}
build() {
Column() {
List({ space: 10 }) {
ForEach(this.arr, (item: number) => {
ListItem() {
Text(`${item}`)
.width('100%')
.height(100)
.fontSize(16)
.textAlign(TextAlign.Center)
.borderRadius(10)
.backgroundColor(0xFFFFFF)
}
.swipeAction({
start: {
builder: overBuilderDemo() // The compilation toolchain automatically binds this context.
},
end: {
builder: () => {
// When the local @Builder is called in the arrow function, the this context is automatically bound and does not need to be processed by the compilation toolchain.
this.privateBuilder()
}
}
})
}, (item: number) => JSON.stringify(item))
}
}
}
}

Multi-Layer @Builder Function Nesting
Invoke customized components or other @Builder functions in the @Builder function to implement nested use of multiple @Builders. To implement the dynamic UI refresh function of the @Builder at the innermost layer, the @Builder must be called by reference at each layer. Here, $$ is not a mandatory parameter. You can replace it with another name.
class ThisTmp {
public paramA1: string = '';
}
@Builder
function parentBuilder($$: ThisTmp) {
Row() {
Column() {
Text(`parentBuilder===${$$.paramA1}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
// Call the custom component HelloComponent.
HelloComponent({ message: $$.paramA1 })
// Call the global @Builder function childBuilder.
childBuilder({ paramA1: $$.paramA1 })
}
}
}
@Component
struct HelloComponent {
@Prop message: string = '';
build() {
Row() {
Text(`HelloComponent===${this.message}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
}
}
}
@Builder
function childBuilder($$: ThisTmp) {
Row() {
Column() {
Text(`childBuilder===${$$.paramA1}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
// Call the custom component HelloChildComponent.
HelloChildComponent({ message: $$.paramA1 })
// Call the global @Builder function grandsonBuilder.
grandsonBuilder({ paramA1: $$.paramA1 })
}
}
}
@Component
struct HelloChildComponent {
@Prop message: string = '';
build() {
Row() {
Text(`HelloChildComponent===${this.message}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
}
}
}
@Builder
function grandsonBuilder($$: ThisTmp) {
Row() {
Column() {
Text(`grandsonBuilder===${$$.paramA1}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
// Call the custom component HelloGrandsonComponent.
HelloGrandsonComponent({ message: $$.paramA1 })
}
}
}
@Component
struct HelloGrandsonComponent {
@Prop message: string;
build() {
Row() {
Text(`HelloGrandsonComponent===${this.message}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
}
}
}
@Entry
@Component
struct ParentExample {
@State label: string = 'Hello';
build() {
Column() {
// Call the global @Builder function parentBuilder.
parentBuilder({ paramA1: this.label })
Button('Click me').onClick(() => {
this.label = 'ArkUI';
})
}
.height('100%')
.width('100%')
}
}

@Builder Function Union V2 Decorator
Class object instances decorated by @ObservedV2 and @Trace have the capability of deeply observing attribute changes. In a custom component decorated by @ComponentV2, when the global builder or local builder is called and parameters are transferred in value transfer mode, modifying the object attributes decorated by @Trace can trigger UI refresh.
@ObservedV2
class Info {
@Trace public name: string;
@Trace public age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
}
@Builder
function overBuilderTest(param: Info) {
Column() {
Text(`Global@Builder name: ${param.name}`)
Text(`Global@Builder age: ${param.age}`)
}
.width(230)
.height(40)
.margin(10)
.padding({ left: 20 })
.backgroundColor('#0d000000')
.borderRadius(20)
}
@ComponentV2
struct ChildPage {
@Require @Param childInfo: Info;
build() {
Column() {
// The value must be transferred. If the reference transfer mode is used, the request will be intercepted by the ArkTS syntax.
overBuilderTest(this.childInfo)
}
}
}
@Entry
@ComponentV2
struct ParentPage {
info1: Info = new Info('Tom', 25);
info2: Info = new Info('Tom', 25);
@Builder
privateBuilder() {
Column() {
Text(`Private@Builder name: ${this.info1.name}`)
Text(`Private@Builder age: ${this.info1.age}`)
}
.width(230)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.borderRadius(20)
}
build() {
Column() {
Flex() {
Column() {
Text(`info1: ${this.info1.name} ${this.info1.age}`) // Text1
Text(`info2: ${this.info2.name} ${this.info2.age}`) // Text2
}
}
.width(230)
.height(40)
.margin(10)
.padding({ left: 60 })
.backgroundColor('#0d000000')
.borderRadius(20)
// Call the local @Builder.
this.privateBuilder()
// Call the global @Builder. The value must be transferred. If the value is transferred by reference, it will be intercepted by the ArkTS syntax.
overBuilderTest(this.info2)
ChildPage({ childInfo: this.info1 }) // Call the custom component.
ChildPage({ childInfo: this.info2 }) // Call the custom component.
Button('change info1&info2')
.onClick(() => {
this.info1.name = 'Cat'; // Change the name value of info1 displayed in Text1.
this.info1.age = 18; // Change the age value of info1 displayed in Text1.
this.info2.name = 'Cat'; // Change the name value of info2 displayed in Text2.
this.info2.age = 18; // Change the age value of info2 displayed in Text2.
})
}
.height('100%')
.width('100%')
}
}

When passing parameters to @Builder by reference, if the parameter is an object decorated with @Local, assigning a value to the entire object triggers a UI refresh within @Builder.
class LocalInfo {
public name: string = 'Tom';
public age: number = 25;
}
@Builder
function overBuilderLocal(param: LocalInfo) {
Column() {
Text(`Global@Builder name: ${param.name}`)
Text(`Global@Builder age: ${param.age}`)
}
.width(230)
.height(40)
.margin(10)
.padding({ left: 20 })
.backgroundColor('#0d000000')
.borderRadius(20)
}
@ComponentV2
struct ChildLocalPage {
@Require @Param childLocalInfo: LocalInfo;
build() {
Column() {
// By-reference parameter passing is used.
overBuilderLocal({ name: this.childLocalInfo.name, age: this.childLocalInfo.age })
}
}
}
@Entry
@ComponentV2
struct ParentLocalPage {
LocalInfo1: LocalInfo = { name: 'Tom', age: 25 };
@Local LocalInfo2: LocalInfo = { name: 'Tom', age: 25 };
@Builder
privateBuilder() {
Column() {
Text(`Private@Builder name: ${this.LocalInfo1.name}`)
Text(`Private@Builder age: ${this.LocalInfo1.age}`)
}
.width(230)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.borderRadius(20)
}
build() {
Column() {
Flex() {
Column() {
Text(`LocalInfo1: ${this.LocalInfo1.name} ${this.LocalInfo1.age}`) // Text1
Text(`LocalInfo2: ${this.LocalInfo2.name} ${this.LocalInfo2.age}`) // Text2
}
}
.width(230)
.height(40)
.margin(10)
.padding({ left: 60 })
.backgroundColor('#0d000000')
.borderRadius(20)
// Call the local @Builder.
this.privateBuilder()
// Call the global @Builder. Here, the reference transfer mode is used.
overBuilderLocal({ name: this.LocalInfo2.name, age: this.LocalInfo2.age })
ChildLocalPage({ childLocalInfo: this.LocalInfo1 }) // Invoke the custom component.
ChildLocalPage({ childLocalInfo: this.LocalInfo2 }) // Invoke the custom component.
Button('change LocalInfo1&LocalInfo2')
.onClick(() => {
this.LocalInfo1 = { name: 'Cat', age: 18 }; // Text1 is not re-rendered because no decorator is used to listen for value changes.
this.LocalInfo2 = { name: 'Cat', age: 18 }; // Text2 is re-rendered because a decorator is used to listen for value changes.
})
}
.height('100%')
.width('100%')
}
}

Global @Builder Reused Across Components
In the cross-component scenario, the global @Builder is called to transfer parameters by reference to implement the dynamic UI update function.
class ReusableTmp {
public componentName: string = 'Child';
}
@Builder
function itemBuilder(params: ReusableTmp) {
Column() {
Text(`Builder ===${params.componentName}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
}
}
@Entry
@Component
struct ReusablePage {
@State switchFlag: boolean = true;
build() {
Column() {
if (this.switchFlag) {
// Invoke the customized component ReusableChildPage.
ReusableChildPage({ message: 'Child' })
} else {
// Invoke the customized component ReusableChildTwoPage.
ReusableChildTwoPage({ message: 'ChildTwo' })
}
Button('Click me')
.onClick(() => {
this.switchFlag = !this.switchFlag;
})
}
.height('100%')
.width('100%')
}
}
@Reusable
@Component
struct ReusableChildPage {
@State message: string = 'Child';
aboutToReuse(params: Record<string, ESObject>): void {
console.info('Recycle ====Child');
this.message = params.message;
}
build() {
Column() {
Text(`ReusableChildPage ===${this.message}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
// Invoke the global @Builder function itemBuilder.
itemBuilder({ componentName: this.message })
}
}
}
@Reusable
@Component
struct ReusableChildTwoPage {
@State message: string = 'ChildTwo';
aboutToReuse(params: Record<string, ESObject>): void {
console.info('Recycle ====ChildTwo');
this.message = params.message;
}
build() {
Column() {
Text(`ReusableChildTwoPage ===${this.message}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
// Invoke the global @Builder function itemBuilder.
itemBuilder({ componentName: this.message })
}
}
}

State Variables Can Be Refreshed in the @Builder
From API version 20, you can use the UIUtils.makeBinding() function, the Binding class, and the MutableBinding class to refresh state variables in the @Builder function. For details, see makeBinding.
import { Binding, MutableBinding, UIUtils } from '@kit.ArkUI';
@ObservedV2
class ClassA {
@Trace public props: string = 'Hello';
}
@Builder
function customButton(num1: Binding<number>, num2: MutableBinding<number>) {
Row() {
Column() {
Text(`number1 === ${num1.value}, number2 === ${num2.value}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
Button(`only change number2`)
.onClick(() => {
num2.value += 1;
})
}
}
}
@Builder
function customButtonObj(obj1: MutableBinding<ClassA>) {
Row() {
Column() {
Text(`props === ${obj1.value.props}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
Button(`change props`)
.onClick(() => {
obj1.value.props += 'Hi';
})
}
}
}
@Entry
@ComponentV2
struct Single {
@Local number1: number = 5;
@Local number2: number = 12;
@Local classA: ClassA = new ClassA();
build() {
Column() {
Button(`change both number1 and number2`)
.onClick(() => {
this.number1 += 1;
this.number2 += 2;
})
Text(`number1 === ${this.number1}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
Text(`number2 === ${this.number2}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
// Call the global @Builder function customButton.
customButton(
UIUtils.makeBinding<number>(() => this.number1), // Use the UIUtils.makeBinding() function to update the state variables in the @Builder function.
UIUtils.makeBinding<number>(
() => this.number2,
(val: number) => {
this.number2 = val;
})
)
Text(`classA.props === ${this.classA.props}`)
.width(300)
.height(40)
.margin(10)
.backgroundColor('#0d000000')
.fontColor('#e6000000')
.borderRadius(20)
.textAlign(TextAlign.Center)
// Call the global @Builder function customButtonObj.
customButtonObj(
UIUtils.makeBinding<ClassA>( // Use the UIUtils.makeBinding () function to refresh state variables in the @Builder function.
() => this.classA,
(val: ClassA) => {
this.classA = val;
})
)
}
.width('100%')
.height('100%')
.alignItems(HorizontalAlign.Center)
.justifyContent(FlexAlign.Center)
}
}

FAQs
Multiple Parameters in @Builder
When @Builder functions have two or more parameters, UI re-rendering is not triggered by value changes, even when parameters are passed via object literals.
Incorrect Usage
class GlobalTmp1 {
public strValue: string = 'Hello';
}
@Builder
function overBuilder1(param: GlobalTmp1, num: number) {
Column() {
Text(`strValue: ${param.strValue}`)
Text(`num: ${num}`)
}
}
@Entry
@Component
struct Parent1 {
@State objParam: GlobalTmp1 = new GlobalTmp1();
@State num: number = 0;
build() {
Column() {
Text('UI Rendered via @Builder')
.fontSize(20)
// Two parameters are used, which is incorrect.
overBuilder1({ strValue: this.objParam.strValue }, this.num)
Line()
.width('100%')
.height(10)
.backgroundColor('#000000').margin(10)
Button('Update Values').onClick(() => {
this.objParam.strValue = 'Hello World';
this.num = 1;
})
}
}
}
Incorrect Usage
class GlobalTmp2 {
public strValue: string = 'Hello';
}
class SecondTmp {
public numValue: number = 0;
}
@Builder
function overBuilder2(param: GlobalTmp2, num: SecondTmp) {
Column() {
Text(`strValue: ${param.strValue}`)
Text(`num: ${num.numValue}`)
}
}
@Entry
@Component
struct Parent2 {
@State strParam: GlobalTmp2 = new GlobalTmp2();
@State numParam: SecondTmp = new SecondTmp();
build() {
Column() {
Text('UI Rendered via @Builder')
.fontSize(20)
// Two parameters are used, which is incorrect.
overBuilder2({ strValue: this.strParam.strValue }, { numValue: this.numParam.numValue })
Line()
.width('100%')
.height(10)
.backgroundColor('#000000').margin(10)
Button('Update Values').onClick(() => {
this.strParam.strValue = 'Hello World';
this.numParam.numValue = 1;
})
}
}
}
@Builder functions accept only one parameter, which must be passed as an object literal. Changes to this parameter's value trigger UI re-rendering.
Correct Usage
class GlobalTmp3 {
public strValue: string = 'Hello';
public numValue: number = 0;
}
@Builder
function overBuilder3(param: GlobalTmp3) {
Column() {
Text(`strValue: ${param.strValue}`)
Text(`num: ${param.numValue}`)
}
}
@Entry
@Component
struct Parent3 {
@State objParam: GlobalTmp3 = new GlobalTmp3();
build() {
Column() {
Text('UI Rendered via @Builder')
.fontSize(20)
// Input a parameter. This is a correct usage.
overBuilder3({ strValue: this.objParam.strValue, numValue: this.objParam.numValue })
Line()
.width('100%')
.height(10)
.backgroundColor('#000000').margin(10)
Button('Update Values').onClick(() => {
this.objParam.strValue = 'Hello World';
this.objParam.numValue = 1;
})
}
}
}
Dynamic Re-rendering with @ComponentV2
In @ComponentV2 decorated components, combine @ObservedV2 and @Trace decorators to enable UI re-rendering through value-based change detection.
Incorrect Usage
In the custom component decorated with @ComponentV2, using primitive data types cannot trigger UI refresh.
@ObservedV2
class ParamTemp {
@Trace public count : number = 0;
}
@Builder
function renderNumber(paramNum: number) {
Text(`paramNum : ${paramNum}`)
.fontSize(30)
.fontWeight(FontWeight.Bold)
}
@Entry
@ComponentV2
struct PageBuilderIncorrectUsage {
@Local classValue: ParamTemp = new ParamTemp();
// Using primitive data type cannot trigger UI re-rendering.
@Local numValue: number = 0;
private progressTimer: number = -1;
aboutToAppear(): void {
this.progressTimer = setInterval(() => {
if (this.classValue.count < 100) {
this.classValue.count += 5;
this.numValue += 5;
} else {
clearInterval(this.progressTimer);
}
}, 500);
}
build() {
Column() {
renderNumber(this.numValue)
}
.width('100%')
.height('100%')
.padding(50)
}
}
Correct Usage
In a custom component decorated by @ComponentV2, only the ParamTmpClass class decorated by @ObservedV2 and the count attribute decorated by @Trace can trigger UI refresh.
@ObservedV2
class ParamTmpClass {
@Trace public count: number = 0;
}
@Builder
function renderText(param: ParamTmpClass) {
Column() {
Text(`param : ${param.count}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
}
@Builder
function renderMap(paramMap: Map<string, number>) {
Text(`paramMap : ${paramMap.get('name')}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
@Builder
function renderSet(paramSet: Set<number>) {
Text(`paramSet : ${paramSet.size}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
@Builder
function renderNumberArr(paramNumArr: number[]) {
Text(`paramNumArr : ${paramNumArr[0]}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
@Entry
@ComponentV2
struct PageBuilderCorrectUsage {
@Local builderParams: ParamTmpClass = new ParamTmpClass();
@Local mapValue: Map<string, number> = new Map();
@Local setValue: Set<number> = new Set([0]);
@Local numArrValue: number[] = [0];
private progressTimer: number = -1;
aboutToAppear(): void {
this.progressTimer = setInterval(() => {
if (this.builderParams.count < 100) {
// builderParams is a ParamTmpClass class decorated with @ObservedV2.
// The count attribute is decorated with @Trace.
// The count change will trigger UI re-rendering.
this.builderParams.count += 5;
this.mapValue.set('name', this.builderParams.count);
this.setValue.add(this.builderParams.count);
this.numArrValue[0] = this.builderParams.count;
} else {
clearInterval(this.progressTimer);
}
}, 500);
}
@Builder
localBuilder() {
Column() {
Text(`localBuilder : ${this.builderParams.count}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
}
build() {
Column() {
this.localBuilder()
Text(`builderParams :${this.builderParams.count}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
renderText(this.builderParams)
renderText({ count: this.builderParams.count })
renderMap(this.mapValue)
renderSet(this.setValue)
renderNumberArr(this.numArrValue)
}
.width('100%')
.height('100%')
}
}
Component Creation in @Builder Prevents Parameter Update Propagation
Create the customized component HelloComponent1 in the parentBuilder1 function, transfer the class object as the parameter, and change the value in the object. The UI does not trigger the refresh function.
Incorrect Usage
class Tmp4 {
public name: string = 'Hello';
public age: number = 16;
}
@Builder
function parentBuilder1(params: Tmp4) {
Row() {
Column() {
Text(`parentBuilder1===${params.name}===${params.age}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
// This is not a reference-based transfer mode. The usage is incorrect. As a result, the UI is not refreshed.
HelloComponent1({ info: params })
}
}
}
@Component
struct HelloComponent1 {
@Prop info: Tmp4 = new Tmp4();
build() {
Row() {
Text(`HelloComponent1===${this.info.name}===${this.info.age}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
}
}
@Entry
@Component
struct ParentPage1 {
@State nameValue: string = 'Zhang San';
@State ageValue: number = 18;
build() {
Column() {
parentBuilder1({ name: this.nameValue, age: this.ageValue })
Button('Click me')
.onClick(() => {
// The modification does not cause the change of HelloComponent1.
this.nameValue = 'Li Si';
this.ageValue = 20;
})
}
.height('100%')
.width('100%')
}
}
Create the customized component HelloComponent2 in the parentBuilder2 function, transfer the parameter in the object literal format, and change the value in the object. The UI triggers the refresh function.
Correct Usage
class Tmp5 {
public name: string = 'Hello';
public age: number = 16;
}
@Builder
function parentBuilder2(params: Tmp5) {
Row() {
Column() {
Text(`parentBuilder2===${params.name}===${params.age}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
// Split the entire object into simple types, which is transferred by reference. Modifying attributes can trigger UI refresh.
HelloComponent2({ childName: params.name, childAge: params.age })
}
}
}
@Component
struct HelloComponent2 {
@Prop childName: string = '';
@Prop childAge: number = 0;
build() {
Row() {
Text(`HelloComponent2===${this.childName}===${this.childAge}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
}
}
}
@Entry
@Component
struct ParentPage2 {
@State nameValue: string = 'Zhang San';
@State ageValue: number = 18;
build() {
Column() {
parentBuilder2({ name: this.nameValue, age: this.ageValue })
Button('Click me')
.onClick(() => {
// Modifying the content here will cause the change of HelloComponent2.
this.nameValue = 'Li Si';
this.ageValue = 20;
})
}
.height('100%')
.width('100%')
}
}
Invoking the @Builder function or method outside the UI statement affects the normal refreshing of the node.
When the @Builder method assigns a value to a variable or array, the method cannot be used in the UI method, and the node display is abnormal during refresh.
Incorrect Usage
@Entry
@Component
struct BackGround1 {
@Builder
myImages() {
Column() {
// Load the image resource named startIcon from the media directory of the application. 'app.media.startIcon' is only an example. Replace it with the actual one.
Image($r('app.media.startIcon')).width('100%').height('100%')
}
};
@Builder
myImages2() {
Column() {
// Load the image resource named startIcon from the media directory of the application. 'app.media.startIcon' is only an example. Replace it with the actual one.
Image($r('app.media.startIcon')).width('100%').height('100%')
}
};
private bgList: Array<CustomBuilder> = [this.myImages(), this.myImages2()]; // Incorrect usage. Do not invoke the @Builder method outside the UI method.
@State bgBuilder: CustomBuilder = this.myImages(); // Incorrect usage. Do not call the @Builder method outside the UI method.
@State bgColor: ResourceColor = Color.Orange;
@State bgColor2: ResourceColor = Color.Orange;
@State index: number = 0;
build() {
Column({ space: 10 }) {
Text('1').width(100).height(50)
Text('2').width(100).height(50)
Text('3').width(100).height(50)
Text('4-1').width(100).height(50).fontColor(this.bgColor)
Text('5-1').width(100).height(50)
Text('4-2').width(100).height(50)
Text('5-2').width(100).height(50)
Stack() {
Column() {
Text('Vsync2')
}
.size({ width: '100%', height: '100%' })
.border({ width: 1, color: Color.Black })
}
.size({ width: 100, height: 80 })
.backgroundColor('#ffbbd4bb')
Button('change').onClick((event: ClickEvent) => {
this.index = 1;
this.bgColor = Color.Red;
this.bgColor2 = Color.Red;
})
}
.margin(10)
}
}
Correct Usage
@Entry
@Component
struct BackGround2 {
@Builder
myImages() {
Column() {
// Load the image resource named startIcon from the media directory of the application. 'app.media.startIcon' is only an example. Replace it with the actual one.
Image($r('app.media.startIcon')).width('100%').height('100%')
}
}
@Builder
myImages2() {
Column() {
// Load the image resource named startIcon from the media directory of the application. 'app.media.startIcon' is only an example. Replace it with the actual one.
Image($r('app.media.startIcon')).width('100%').height('100%')
}
}
@State bgColor: ResourceColor = Color.Orange;
@State bgColor2: ResourceColor = Color.Orange;
@State index: number = 0;
build() {
Column({ space: 10 }) {
Text('1').width(100).height(50)
Text('2').width(100).height(50).background(this.myImages) // Directly pass the @Builder method.
Text('3').width(100).height(50).background(this.myImages()) // Directly calls the @Builder method.
Text('4-1').width(100).height(50).fontColor(this.bgColor)
Text('5-1').width(100).height(50)
Text('4-2').width(100).height(50)
Text('5-2').width(100).height(50)
Stack() {
Column() {
Text('Vsync2')
}
.size({ width: '100%', height: '100%' })
.border({ width: 1, color: Color.Black })
}
.size({ width: 100, height: 80 })
.backgroundColor('#ffbbd4bb')
Button('change').onClick((event: ClickEvent) => {
this.index = 1;
this.bgColor = Color.Red;
this.bgColor2 = Color.Red;
})
}
.margin(10)
}
}
The Set Accessor Is Not Passed When MutableBinding Is Used in the @Builder Method
When MutableBinding is used in the @Builder method definition, the set accessor is not passed to the MutableBinding type parameter during construction. As a result, a runtime error occurs when the set accessor is triggered.
Incorrect Usage
import { UIUtils, Binding, MutableBinding } from '@kit.ArkUI';
@ObservedV2
class GlobalTmp1 {
@Trace public strValue: string = 'Hello';
}
@Builder
function builderWithTwoParams1(param1: Binding<GlobalTmp1>, param2: MutableBinding<number>) {
Column() {
Text(`strValue: ${param1.value.strValue}`)
Button(`num: ${param2.value}`)
.onClick(() => {
param2.value += 1; // Click the button to trigger the set accessor, which causes a runtime error.
})
}.borderWidth(1)
}
@Entry
@ComponentV2
struct MakeBindingTest1 {
@Local GlobalTmp1: GlobalTmp1 = new GlobalTmp1();
@Local num: number = 0;
build() {
Column() {
Text(`${this.GlobalTmp1.strValue}`)
builderWithTwoParams1(UIUtils.makeBinding(() => this.GlobalTmp1),
UIUtils.makeBinding<number>(() => this.num)) // SetterCallback is not passed when a parameter of the MutableBinding type is constructed.
Button('Update Values').onClick(() => {
this.GlobalTmp1.strValue = 'Hello World 2025';
this.num = 1;
})
}
}
}
Correct Usage
import { UIUtils, Binding, MutableBinding } from '@kit.ArkUI';
@ObservedV2
class GlobalTmp2 {
@Trace public strValue: string = 'Hello';
}
@Builder
function builderWithTwoParams2(param1: Binding<GlobalTmp2>, param2: MutableBinding<number>) {
Column() {
Text(`strValue: ${param1.value.strValue}`)
Button(`num: ${param2.value}`)
.onClick(() => {
param2.value += 1; // The value attribute of the MutableBinding parameter is modified.
})
}.borderWidth(1)
}
@Entry
@ComponentV2
struct MakeBindingTest2 {
@Local GlobalTmp2: GlobalTmp2 = new GlobalTmp2();
@Local num: number = 0;
build() {
Column() {
Text(`${this.GlobalTmp2.strValue}`)
builderWithTwoParams2(UIUtils.makeBinding(() => this.GlobalTmp2),
UIUtils.makeBinding<number>(() => this.num,
val => {
this.num = val;
}))
Button('Update Values').onClick(() => {
this.GlobalTmp2.strValue = 'Hello World 2025';
this.num = 1;
})
}
}
}
Changing the Input Parameters in the @Builder Decorated Function
If MutableBinding is not used, the modification of parameter values in the function decorated by @Builder does not take effect and may cause runtime errors. Since API version 23, error code 140109 will be returned.
Incorrect Usage
@Builder
function myGlobalBuilder(value: string) {
Column() {
Text(`myGlobalBuilder: ${value} `)
.fontSize(16)
.onClick(() => {
// Modify parameters in the @Builder function of the simple type transferred by value. The UI does not refresh but does not exit unexpectedly.
value = 'value change';
})
}.borderWidth(1)
}
interface TempMod1 {
paramA: string;
}
@Builder
function overBuilderMod1(param: TempMod1) {
Row() {
Column() {
Button(`overBuilderMod1 === ${param.paramA}`)
.onClick(() => {
// Incorrect format. The attributes of object type parameters cannot be modified in the function decorated by @Builder. As a result, the system crashes and the UI is not refreshed.
param.paramA = 'Yes';
})
Button('change')
.onClick(() => {
// Incorrect format. The reference of the object type parameter cannot be modified in the function decorated by @Builder. The UI does not refresh but does not exit unexpectedly.
param = { paramA: 'change trial' };
})
}
}
}
@Entry
@Component
struct ParentMod1 {
@State label: string = 'Hello';
@State message1: string = 'Value Passing';
@Builder
extendBlank() {
Row() {
Blank()
}
.height(20)
}
build() {
Column() {
// Passing by reference can implement UI refresh when parameters change, but parameters cannot be modified in the @Builder function.
overBuilderMod1({ paramA: this.label });
this.extendBlank();
Button('click me')
.onClick(() => {
this.label = 'ArkUI';
})
this.extendBlank();
myGlobalBuilder(this.message1);
}
}
}
Correct Usage
import { UIUtils, MutableBinding } from '@kit.ArkUI';
// Use MutableBinding to modify parameter values in the @Builder decorated function.
@Builder
function myGlobalBuilderMod(str: MutableBinding<string>) {
Column() {
Text(`Mod--MyGlobalBuilder: ${str.value}`)
.fontSize(16)
.onClick(() => {
str.value = 'value change mod';
})
}
}
interface TempMod2 {
paramA: string;
}
// Use MutableBinding to modify parameter values in the function decorated by @Builder.
@Builder
function overBuilderMod2(param: MutableBinding<TempMod2>) {
Column() {
Button(`Mod--overBuilder === ${param.value.paramA}`)
.onClick(() => {
param.value.paramA = 'Yes';
})
Button(`change`)
.onClick(() => {
param.value = { paramA: 'trialOne' };
})
}
}
@Entry
@Component
struct ParentMod2 {
@State label: string = 'Hello';
@State message1: string = 'Value Passing';
@State objectOne: TempMod2 = {
paramA: this.label
};
@Builder
extendBlank() {
Row() {
Blank()
}
.height(20)
}
build() {
Column() {
// When MutableBinding is used, the object literal cannot be transferred. You need to extract the literal object as a state variable first.
overBuilderMod2(
UIUtils.makeBinding<TempMod2>(
() => this.objectOne,
value => {
this.objectOne = value; // SetterCallback must be transferred. Otherwise, a runtime error occurs when this function is triggered.
}
)
)
this.extendBlank();
Button('click me')
.onClick(() => {
this.objectOne.paramA = 'ArkUI';
})
this.extendBlank();
myGlobalBuilderMod(
UIUtils.makeBinding<string>(
() => this.message1,
value => {
this.message1 = value; // SetterCallback must be transferred. Otherwise, a runtime error occurs when this function is triggered.
}
)
);
}
}
}
Execute the @Builder function in the @Watch function.
If the @Builder function is executed in the @Watch function, the UI refresh is abnormal.
Incorrect Usage
@Entry
@Component
struct Child1 {
@Provide @Watch('provideWatch') content: string = 'Index: hello world';
@Builder
watchBuilder(content: string) {
Row() {
Text(`${content}`)
}
}
provideWatch() {
this.watchBuilder(this.content); // Incorrect format. Use the @Builder function in the @Watch function.
}
build() {
Column() {
Button(`content value: ${this.content}`)
.onClick(() => {
this.content += '_world';
})
this.watchBuilder(this.content);
}
}
}
Correct Usage
@Entry
@Component
struct Child2 {
@Provide @Watch('provideWatch') content: string = 'Index: hello world';
@Builder
watchBuilder(content: string) {
Row() {
Text(`${content}`)
}
}
provideWatch() {
// Correct format. Do not use the @Builder function in the @Watch function.
console.info(`content value has changed.`);
}
build() {
Column() {
Button(`content value: ${this.content}`)
.onClick(() => {
this.content += '_world';
})
this.watchBuilder(this.content);
}
}
}