1# Custom Declarative Node (BuilderNode)
2
3## Overview
4
5[BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md) is a custom declarative nodedesigned to seamlessly mount built-in components. With BuilderNode, you can build a custom component tree within stateless UI environments through the [global custom builder function](../quick-start/arkts-builder.md#global-custom-builder-function), which is decorated by @Builder. Once your custom component tree is established, you can obtain its root [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md) by calling [getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode). The root node can be directly returned by [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md) and mounted under a [NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md). **BuilderNode** facilitates embedding of embedding declarative components within **FrameNode** and [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md) trees for mixed display. **BuilderNode** also offers a feature for exporting textures, which can be used for rendering within the same layer of the [XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md).
6
7The ArkTS built-in component tree constructed by **BuilderNode** can be used together with custom nodes, such as FrameNodes and RenderNodes, to achieve the mixed display effect. **BuilderNode** offers a suite of APIs designed to integrate built-in components within third-party frameworks. This is particularly beneficial for scenarios where these frameworks require interaction with custom nodes
8
9**BuilderNode** offers the capability to pre-create components, allowing you to dictate when built-in components are instantiated. This feature is useful for dynamically mounting and displaying components, especially for those that have a longer initialization period, such as [Web](../reference/apis-arkweb/ts-basic-components-web.md) and [XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md).
10
11![builder-node](figures/builder-node.png)
12
13## Basic Concepts
14
15- [Built-in component](arkts-ui-development-overview.md): component provided directly by ArkUI. Components are essential elements of the UI, working together to shape the UI.
16
17- Entity node: native node created by the backend.
18
19A BuilderNode can be used only as a leaf node. If an update is required, you are advised to use the [update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update) API provided by the BuilderNode, rather than making modifications directly to the RenderNode obtained from it.
20
21> **NOTE**
22>
23> - The BuilderNode only supports a single [global custom build function(../quick-start/arkts-builder.md#global-custom-builder-function) decorated by @Builder and wrapped by [wrapBuilder](../quick-start/arkts-wrapBuilder.md).
24>
25> - A newly created BuilderNode can only obtain a **FrameNode** object pointing to the root node through [getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode) after [build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build); otherwise, it returns **null**.
26>
27> - If the root node of the passed Builder is a syntactic node (such as **if/else** and **ForEach**), an additional FrameNode must be generated, which will be displayed as "BuilderProxyNode" in the node tree.
28>
29> - If BuilderNode mounts a node onto another FrameNode through **getFrameNode**, or mounts it as a child node onto a **NodeContainer**, the node uses the layout constraints of the parent component for layout.
30>
31> - If a BuilderNode's FrameNode mounts its node onto a RenderNode through [getRenderNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#getrendernode), its size defaults to **0** since its FrameNode is not yet part of the tree. To display it properly, you must explicitly specify the layout constraint size through [selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions) in the constructor.
32>
33> - Pre-creation with the BuilderNode does not reduce the creation time of components. For the **Web** component, resources must be loaded in the kernel during creation, and pre-creation cannot reduce this time. However, it enables the kernel to preload resources, which can reduce the loading time when the component is used.
34
35## Creating a BuilderNode Object
36
37When creating a **BuilderNode** object, which is a template class, you must specify a type that matches the type of the [WrappedBuilder](../quick-start/arkts-wrapBuilder.md) used in the **build** method later on. Mismatches can cause compilation warnings and failures.
38
39## Creating a Built-in Component Tree
40
41Use the **build** API of **BuilderNode** to create a built-in component tree. The tree is constructed based on the **WrappedBuilder** object passed in, and the root node of the component tree is retained.
42
43> **NOTE**
44>
45> Stateless UI methods using the global @Builder can have at most one root node.
46>
47> The @Builder within the **build** method accepts only one input parameter.
48>
49> In scenarios where @Builder is nested within another @Builder in the **build** method, ensure that the parameters of the nested @Builder match the input parameters provided to the **build** method.
50>
51> For scenarios where @Builder is nested within another @Builder, if the parameter types do not match, you must include the [BuilderOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#buildoptions12) field as a parameter for the [build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build12) method.
52>
53> To operate objects in a BuilderNode, ensure that the reference to the BuilderNode is not garbage collected. Once a BuilderNode object is collected by the virtual machine, its FrameNode and RenderNode objects will also be dereferenced from the backend nodes. This means that any FrameNode objects obtained from a BuilderNode will no longer correspond to any actual node if the BuilderNode is garbage collected.
54
55Create offline nodes and built-in component trees, and use them in conjunction with FrameNodes.
56
57The root node of the BuilderNode is directly used as the return value of [makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode) of [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md).
58
59```ts
60import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
61
62class Params {
63  text: string = "";
64
65  constructor(text: string) {
66    this.text = text;
67  }
68}
69
70@Builder
71function buildText(params: Params) {
72  Column() {
73    Text(params.text)
74      .fontSize(50)
75      .fontWeight(FontWeight.Bold)
76      .margin({ bottom: 36 })
77  }
78}
79
80class TextNodeController extends NodeController {
81  private textNode: BuilderNode<[Params]> | null = null;
82  private message: string = "DEFAULT";
83
84  constructor(message: string) {
85    super();
86    this.message = message;
87  }
88
89  makeNode(context: UIContext): FrameNode | null {
90    this.textNode = new BuilderNode(context);
91    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
92    return this.textNode.getFrameNode();
93  }
94}
95
96@Entry
97@Component
98struct Index {
99  @State message: string = "hello";
100
101  build() {
102    Row() {
103      Column() {
104        NodeContainer(new TextNodeController(this.message))
105          .width('100%')
106          .height(100)
107          .backgroundColor('#FFF0F0F0')
108      }
109      .width('100%')
110      .height('100%')
111    }
112    .height('100%')
113  }
114}
115```
116
117When combining a BuilderNode with a RenderNode, note the following:
118
119If you mount the RenderNode from the BuilderNode under another RenderNode, you must explicitly specify [selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions) as the layout constraint for the BuilderNode. This approach to mounting nodes is not recommended.
120
121```ts
122import { NodeController, BuilderNode, FrameNode, UIContext, RenderNode } from "@kit.ArkUI";
123
124class Params {
125  text: string = "";
126
127  constructor(text: string) {
128    this.text = text;
129  }
130}
131
132@Builder
133function buildText(params: Params) {
134  Column() {
135    Text(params.text)
136      .fontSize(50)
137      .fontWeight(FontWeight.Bold)
138      .margin({ bottom: 36 })
139  }
140}
141
142class TextNodeController extends NodeController {
143  private rootNode: FrameNode | null = null;
144  private textNode: BuilderNode<[Params]> | null = null;
145  private message: string = "DEFAULT";
146
147  constructor(message: string) {
148    super();
149    this.message = message;
150  }
151
152  makeNode(context: UIContext): FrameNode | null {
153    this.rootNode = new FrameNode(context);
154    let renderNode = new RenderNode();
155    renderNode.clipToFrame = false;
156    this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } });
157    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
158    const textRenderNode = this.textNode?.getFrameNode()?.getRenderNode();
159
160    const rootRenderNode = this.rootNode.getRenderNode();
161    if (rootRenderNode !== null) {
162      rootRenderNode.appendChild(renderNode);
163      renderNode.appendChild(textRenderNode);
164    }
165
166    return this.rootNode;
167  }
168}
169
170@Entry
171@Component
172struct Index {
173  @State message: string = "hello";
174
175  build() {
176    Row() {
177      Column() {
178        NodeContainer(new TextNodeController(this.message))
179          .width('100%')
180          .height(100)
181          .backgroundColor('#FFF0F0F0')
182      }
183      .width('100%')
184      .height('100%')
185    }
186    .height('100%')
187  }
188}
189```
190
191## Updating the Built-in Component Tree
192
193Create a built-in component tree using the **build** API of a **BuilderNode** object. The tree is constructed based on the **WrappedBuilder** object passed in, and the root node of the component tree is retained.
194
195Custom component updates follow the update mechanisms of [state management](../quick-start/arkts-state-management-overview.md). For custom components used directly in a **WrappedBuilder** object, their parent component is the **BuilderNode** object. Therefore, to update child components defined in the **WrappedBuilder** objects, you need to define the relevant state variables with the [\@Prop](../quick-start/arkts-prop.md) or [\@ObjectLink](../quick-start/arkts-observed-and-objectlink.md) decorator, in accordance with the specifications of state management and the needs of your application development.
196
197To update nodes within a BuilderNode:
198
199- Use the **update** API to update individual nodes within the BuilderNode.
200- Use the [updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12) API to trigger a full update of all nodes within the BuilderNode.
201
202
203
204```ts
205import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
206
207class Params {
208  text: string = "";
209  constructor(text: string) {
210    this.text = text;
211  }
212}
213
214// Custom component
215@Component
216struct TextBuilder {
217  // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute.
218  @Prop message: string = "TextBuilder";
219
220  build() {
221    Row() {
222      Column() {
223        Text(this.message)
224          .fontSize(50)
225          .fontWeight(FontWeight.Bold)
226          .margin({ bottom: 36 })
227          .backgroundColor(Color.Gray)
228      }
229    }
230  }
231}
232
233@Builder
234function buildText(params: Params) {
235  Column() {
236    Text(params.text)
237      .fontSize(50)
238      .fontWeight(FontWeight.Bold)
239      .margin({ bottom: 36 })
240    TextBuilder({ message: params.text }) // Custom component
241  }
242}
243
244class TextNodeController extends NodeController {
245  private textNode: BuilderNode<[Params]> | null = null;
246  private message: string = "";
247
248  constructor(message: string) {
249    super()
250    this.message = message
251  }
252
253  makeNode(context: UIContext): FrameNode | null {
254    this.textNode = new BuilderNode(context);
255    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
256    return this.textNode.getFrameNode();
257  }
258
259  update(message: string) {
260    if (this.textNode !== null) {
261      // Call update to perform an update.
262      this.textNode.update(new Params(message));
263    }
264  }
265}
266
267@Entry
268@Component
269struct Index {
270  @State message: string = "hello";
271  private textNodeController: TextNodeController = new TextNodeController(this.message);
272  private count = 0;
273
274  build() {
275    Row() {
276      Column() {
277        NodeContainer(this.textNodeController)
278          .width('100%')
279          .height(200)
280          .backgroundColor('#FFF0F0F0')
281        Button('Update')
282          .onClick(() => {
283            this.count += 1;
284            const message = "Update " + this.count.toString();
285            this.textNodeController.update(message);
286          })
287      }
288      .width('100%')
289      .height('100%')
290    }
291    .height('100%')
292  }
293}
294```
295
296## Canceling the Reference to the Entity Node
297
298A **BuilderNode** object is mapped to a backend entity node, and its memory release is usually contingent on the disposal of the frontend object. To directly release the backend node object, you can call the [dispose](../reference/apis-arkui/js-apis-arkui-builderNode.md#dispose12) API to break the reference to the entity node. Once this is done, the frontend **BuilderNode** object will no longer affect the lifecycle of the entity node.
299
300> **NOTE**
301>
302> Calling **dispose** on a **BuilderNode** object breaks its reference to the backend entity node, and also simultaneously severs the references of its contained FrameNode and RenderNode to their respective entity nodes.
303
304## Injecting a Touch Event
305
306Use the [postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent) API in the BuilderNode to inject a [touch event](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md) into the bound component for event simulation and forwarding.
307
308
309
310The following example forwards a touch event from one **Column** component to another in the BuilderNode, so that when the lower **Column** component is touched, the upper **Column** component also receives the same touch event. The API returns **true** if the button's event is successfully recognized.
311
312```ts
313import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';
314
315class Params {
316  text: string = "this is a text";
317}
318
319@Builder
320function ButtonBuilder(params: Params) {
321  Column() {
322    Button(`button ` + params.text)
323      .borderWidth(2)
324      .backgroundColor(Color.Orange)
325      .width("100%")
326      .height("100%")
327      .gesture(
328        TapGesture()
329          .onAction((event: GestureEvent) => {
330            console.log("TapGesture");
331          })
332      )
333  }
334  .width(500)
335  .height(300)
336  .backgroundColor(Color.Gray)
337}
338
339class MyNodeController extends NodeController {
340  private rootNode: BuilderNode<[Params]> | null = null;
341  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder);
342
343  makeNode(uiContext: UIContext): FrameNode | null {
344    this.rootNode = new BuilderNode(uiContext);
345    this.rootNode.build(this.wrapBuilder, { text: "this is a string" })
346    return this.rootNode.getFrameNode();
347  }
348
349  postTouchEvent(touchEvent: TouchEvent): void {
350    if (this.rootNode == null) {
351      return;
352    }
353    let result = this.rootNode.postTouchEvent(touchEvent);
354    console.log("result " + result);
355  }
356}
357
358@Entry
359@Component
360struct MyComponent {
361  private nodeController: MyNodeController = new MyNodeController();
362
363  build() {
364    Column() {
365      NodeContainer(this.nodeController)
366        .height(300)
367        .width(500)
368      Column()
369        .width(500)
370        .height(300)
371        .backgroundColor(Color.Pink)
372        .onTouch((event) => {
373          if (event != undefined) {
374            this.nodeController.postTouchEvent(event);
375          }
376        })
377    }
378  }
379}
380```
381
382## Reusing a BuilderNode
383
384To reuse a BuilderNode, pass the [reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12) and [recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12) events to the custom components within the BuilderNode.
385
386```ts
387import { FrameNode,NodeController,BuilderNode,UIContext } from "@kit.ArkUI";
388
389class MyDataSource {
390  private dataArray: string[] = [];
391  private listener: DataChangeListener | null = null
392
393  public totalCount(): number {
394    return this.dataArray.length;
395  }
396
397  public getData(index: number) {
398    return this.dataArray[index];
399  }
400
401  public pushData(data: string) {
402    this.dataArray.push(data);
403  }
404
405  public reloadListener(): void {
406    this.listener?.onDataReloaded();
407  }
408
409  public registerDataChangeListener(listener: DataChangeListener): void {
410    this.listener = listener;
411  }
412
413  public unregisterDataChangeListener(): void {
414    this.listener = null;
415  }
416}
417
418class Params {
419  item: string = '';
420
421  constructor(item: string) {
422    this.item = item;
423  }
424}
425
426@Builder
427function buildNode(param: Params = new Params("hello")) {
428  ReusableChildComponent2({ item: param.item });
429}
430
431class MyNodeController extends NodeController {
432  public builderNode: BuilderNode<[Params]> | null = null;
433  public item: string = "";
434
435  makeNode(uiContext: UIContext): FrameNode | null {
436    if (this.builderNode == null) {
437      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
438      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
439    }
440    return this.builderNode.getFrameNode();
441  }
442}
443
444@Reusable
445@Component
446struct ReusableChildComponent {
447  @State item: string = '';
448  private controller: MyNodeController = new MyNodeController();
449
450  aboutToAppear() {
451    this.controller.item = this.item;
452  }
453
454  aboutToRecycle(): void {
455    console.log("ReusableChildComponent aboutToRecycle " + this.item);
456    this.controller?.builderNode?.recycle();
457  }
458
459  aboutToReuse(params: object): void {
460    console.log("ReusableChildComponent aboutToReuse " + JSON.stringify(params));
461    this.controller?.builderNode?.reuse(params);
462  }
463
464  build() {
465    NodeContainer(this.controller);
466  }
467}
468
469@Component
470struct ReusableChildComponent2 {
471  @Prop item: string = "false";
472
473  aboutToReuse(params: Record<string, object>) {
474    console.log("ReusableChildComponent2 Reusable 2 " + JSON.stringify(params));
475  }
476
477  aboutToRecycle(): void {
478    console.log("ReusableChildComponent2 aboutToRecycle 2 " + this.item);
479  }
480
481  build() {
482    Row() {
483      Text(this.item)
484        .fontSize(20)
485        .backgroundColor(Color.Yellow)
486        .margin({ left: 10 })
487    }.margin({ left: 10, right: 10 })
488  }
489}
490
491
492@Entry
493@Component
494struct Index {
495  @State data: MyDataSource = new MyDataSource();
496
497  aboutToAppear() {
498    for (let i = 0;i < 100; i++) {
499      this.data.pushData(i.toString());
500    }
501  }
502
503  build() {
504    Column() {
505      List({ space: 3 }) {
506        LazyForEach(this.data, (item: string) => {
507          ListItem() {
508            ReusableChildComponent({ item: item })
509          }
510        }, (item: string) => item)
511      }
512      .width('100%')
513      .height('100%')
514    }
515  }
516}
517```
518
519## Updating Nodes Based on System Environment Changes
520
521Use the [updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12) API to listen for [system environment changes](../reference/apis-ability-kit/js-apis-app-ability-configuration.md). This will trigger a full update of all nodes within the BuilderNode.
522
523> **NOTE**
524>
525> The **updateConfiguration** API is designed to inform objects of the need to update, with the updates reflecting changes in the application's current system environment.
526
527```ts
528import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
529import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit';
530
531class Params {
532  text: string = ""
533
534  constructor(text: string) {
535    this.text = text;
536  }
537}
538
539// Custom component
540@Component
541struct TextBuilder {
542  // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute.
543  @Prop message: string = "TextBuilder";
544
545  build() {
546    Row() {
547      Column() {
548        Text(this.message)
549          .fontSize(50)
550          .fontWeight(FontWeight.Bold)
551          .margin({ bottom: 36 })
552          .fontColor($r(`app.color.text_color`))
553          .backgroundColor($r(`app.color.start_window_background`))
554      }
555    }
556  }
557}
558
559@Builder
560function buildText(params: Params) {
561  Column() {
562    Text(params.text)
563      .fontSize(50)
564      .fontWeight(FontWeight.Bold)
565      .margin({ bottom: 36 })
566      .fontColor($r(`app.color.text_color`))
567    TextBuilder({ message: params.text }) // Custom component
568  }.backgroundColor($r(`app.color.start_window_background`))
569}
570
571class TextNodeController extends NodeController {
572  private textNode: BuilderNode<[Params]> | null = null;
573  private message: string = "";
574
575  constructor(message: string) {
576    super()
577    this.message = message;
578  }
579
580  makeNode(context: UIContext): FrameNode | null {
581    return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null;
582  }
583
584  createNode(context: UIContext) {
585    this.textNode = new BuilderNode(context);
586    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
587    builderNodeMap.push(this.textNode);
588  }
589
590  deleteNode() {
591    let node = builderNodeMap.pop();
592    node?.dispose();
593  }
594
595  update(message: string) {
596    if (this.textNode !== null) {
597      // Call update to perform an update.
598      this.textNode.update(new Params(message));
599    }
600  }
601}
602
603// Record the created custom node object.
604const builderNodeMap: Array<BuilderNode<[Params]>> = new Array();
605
606function updateColorMode() {
607  builderNodeMap.forEach((value, index) => {
608    // Notify BuilderNode of the environment changes.
609    value.updateConfiguration();
610  })
611}
612
613@Entry
614@Component
615struct Index {
616  @State message: string = "hello"
617  private textNodeController: TextNodeController = new TextNodeController(this.message);
618  private count = 0;
619
620  aboutToAppear(): void {
621    let environmentCallback: EnvironmentCallback = {
622      onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => {
623        console.log('onMemoryLevel');
624      },
625      onConfigurationUpdated: (config: Configuration): void => {
626        console.log('onConfigurationUpdated ' + JSON.stringify(config));
627        updateColorMode();
628      }
629    }
630    // Register a callback.
631    this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback);
632    // Create a custom node and add it to the map.
633    this.textNodeController.createNode(this.getUIContext());
634  }
635
636  aboutToDisappear(): void {
637    // Remove the reference to the custom node from the map and release the node.
638    this.textNodeController.deleteNode();
639  }
640
641  build() {
642    Row() {
643      Column() {
644        NodeContainer(this.textNodeController)
645          .width('100%')
646          .height(200)
647          .backgroundColor('#FFF0F0F0')
648        Button('Update')
649          .onClick(() => {
650            this.count += 1;
651            const message = "Update " + this.count.toString();
652            this.textNodeController.update(message);
653          })
654      }
655      .width('100%')
656      .height('100%')
657    }
658    .height('100%')
659  }
660}
661```
662