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 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