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 197 198To update nodes within a BuilderNode:<br>Use the **update** API to update individual nodes within the BuilderNode. 199 200Use 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> If the frontend object BuilderNode cannot be released, memory leaks may occur. To avoid this, be sure to call **dispose** on the BuilderNode when you no longer need it. This reduces the complexity of reference relationships and lowers the risk of memory leaks. 305 306## Injecting a Touch Event 307 308Use 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. 309 310 311 312The 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. 313 314```ts 315import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI'; 316 317class Params { 318 text: string = "this is a text"; 319} 320 321@Builder 322function ButtonBuilder(params: Params) { 323 Column() { 324 Button(`button ` + params.text) 325 .borderWidth(2) 326 .backgroundColor(Color.Orange) 327 .width("100%") 328 .height("100%") 329 .gesture( 330 TapGesture() 331 .onAction((event: GestureEvent) => { 332 console.log("TapGesture"); 333 }) 334 ) 335 } 336 .width(500) 337 .height(300) 338 .backgroundColor(Color.Gray) 339} 340 341class MyNodeController extends NodeController { 342 private rootNode: BuilderNode<[Params]> | null = null; 343 private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder); 344 345 makeNode(uiContext: UIContext): FrameNode | null { 346 this.rootNode = new BuilderNode(uiContext); 347 this.rootNode.build(this.wrapBuilder, { text: "this is a string" }) 348 return this.rootNode.getFrameNode(); 349 } 350 351 postTouchEvent(touchEvent: TouchEvent): void { 352 if (this.rootNode == null) { 353 return; 354 } 355 let result = this.rootNode.postTouchEvent(touchEvent); 356 console.log("result " + result); 357 } 358} 359 360@Entry 361@Component 362struct MyComponent { 363 private nodeController: MyNodeController = new MyNodeController(); 364 365 build() { 366 Column() { 367 NodeContainer(this.nodeController) 368 .height(300) 369 .width(500) 370 Column() 371 .width(500) 372 .height(300) 373 .backgroundColor(Color.Pink) 374 .onTouch((event) => { 375 if (event != undefined) { 376 this.nodeController.postTouchEvent(event); 377 } 378 }) 379 } 380 } 381} 382``` 383 384## Reusing a BuilderNode 385 386To implement component reuse within a BuilderNode, you need to call the [reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12) and [recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12) APIs. These APIs pass reuse and recycle events to custom components inside the BuilderNode. 387 388For example, in the following demo, the custom component **ReusableChildComponent** can pass reuse and recycle events to its nested custom component **ReusableChildComponent3**. However, these events cannot automatically reach another custom component, **ReusableChildComponent2**, if it is separated by a BuilderNode. To enable reuse for **ReusableChildComponent2**, you must explicitly call the **reuse** and **recycle** APIs on the BuilderNode to forward these events to **ReusableChildComponent2**. 389 390 391 392```ts 393import { FrameNode, NodeController, BuilderNode, UIContext } from "@kit.ArkUI"; 394 395const TEST_TAG: string = "Reuse+Recycle"; 396 397class MyDataSource { 398 private dataArray: string[] = []; 399 private listener: DataChangeListener | null = null 400 401 public totalCount(): number { 402 return this.dataArray.length; 403 } 404 405 public getData(index: number) { 406 return this.dataArray[index]; 407 } 408 409 public pushData(data: string) { 410 this.dataArray.push(data); 411 } 412 413 public reloadListener(): void { 414 this.listener?.onDataReloaded(); 415 } 416 417 public registerDataChangeListener(listener: DataChangeListener): void { 418 this.listener = listener; 419 } 420 421 public unregisterDataChangeListener(): void { 422 this.listener = null; 423 } 424} 425 426class Params { 427 item: string = ''; 428 429 constructor(item: string) { 430 this.item = item; 431 } 432} 433 434@Builder 435function buildNode(param: Params = new Params("hello")) { 436 Row() { 437 Text(`C${param.item} -- `) 438 ReusableChildComponent2({ item: param.item }) // This custom component cannot be correctly reused in the BuilderNode. 439 } 440} 441 442class MyNodeController extends NodeController { 443 public builderNode: BuilderNode<[Params]> | null = null; 444 public item: string = ""; 445 446 makeNode(uiContext: UIContext): FrameNode | null { 447 if (this.builderNode == null) { 448 this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } }); 449 this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item)); 450 } 451 return this.builderNode.getFrameNode(); 452 } 453} 454 455// The custom component that is reused and recycled will have its state variables updated, and the state variables of the nested custom component ReusableChildComponent3 will also be updated. However, the BuilderNode will block this propagation process. 456@Reusable 457@Component 458struct ReusableChildComponent { 459 @Prop item: string = ''; 460 @Prop switch: string = ''; 461 private controller: MyNodeController = new MyNodeController(); 462 463 aboutToAppear() { 464 this.controller.item = this.item; 465 } 466 467 aboutToRecycle(): void { 468 console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`); 469 470 // When the switch is open, pass the recycle event to the nested custom component, such as ReusableChildComponent2, through the BuilderNode's recycle API to complete recycling. 471 if (this.switch === 'open') { 472 this.controller?.builderNode?.recycle(); 473 } 474 } 475 476 aboutToReuse(params: object): void { 477 console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`); 478 479 // When the switch is open, pass the reuse event to the nested custom component, such as ReusableChildComponent2, through the BuilderNode's reuse API to complete reuse. 480 if (this.switch === 'open') { 481 this.controller?.builderNode?.reuse(params); 482 } 483 } 484 485 build() { 486 Row() { 487 Text(`A${this.item}--`) 488 ReusableChildComponent3({ item: this.item }) 489 NodeContainer(this.controller); 490 } 491 } 492} 493 494@Component 495struct ReusableChildComponent2 { 496 @Prop item: string = "false"; 497 498 aboutToReuse(params: Record<string, object>) { 499 console.log(`${TEST_TAG} ReusableChildComponent2 aboutToReuse ${JSON.stringify(params)}`); 500 } 501 502 aboutToRecycle(): void { 503 console.log(`${TEST_TAG} ReusableChildComponent2 aboutToRecycle ${this.item}`); 504 } 505 506 build() { 507 Row() { 508 Text(`D${this.item}`) 509 .fontSize(20) 510 .backgroundColor(Color.Yellow) 511 .margin({ left: 10 }) 512 }.margin({ left: 10, right: 10 }) 513 } 514} 515 516@Component 517struct ReusableChildComponent3 { 518 @Prop item: string = "false"; 519 520 aboutToReuse(params: Record<string, object>) { 521 console.log(`${TEST_TAG} ReusableChildComponent3 aboutToReuse ${JSON.stringify(params)}`); 522 } 523 524 aboutToRecycle(): void { 525 console.log(`${TEST_TAG} ReusableChildComponent3 aboutToRecycle ${this.item}`); 526 } 527 528 build() { 529 Row() { 530 Text(`B${this.item}`) 531 .fontSize(20) 532 .backgroundColor(Color.Yellow) 533 .margin({ left: 10 }) 534 }.margin({ left: 10, right: 10 }) 535 } 536} 537 538 539@Entry 540@Component 541struct Index { 542 @State data: MyDataSource = new MyDataSource(); 543 544 aboutToAppear() { 545 for (let i = 0; i < 100; i++) { 546 this.data.pushData(i.toString()); 547 } 548 } 549 550 build() { 551 Column() { 552 List({ space: 3 }) { 553 LazyForEach(this.data, (item: string) => { 554 ListItem() { 555 ReusableChildComponent({ 556 item: item, 557 switch: 'open' // Changing open to close can be used to observe the behavior of custom components inside the BuilderNode when reuse and recycle events are not passed through the BuilderNode's reuse and recycle APIs. 558 }) 559 } 560 }, (item: string) => item) 561 } 562 .width('100%') 563 .height('100%') 564 } 565 } 566} 567``` 568 569 570## Updating Nodes Based on System Environment Changes 571 572Use the [updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12) 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. 573 574> **NOTE** 575> 576> 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. 577 578```ts 579import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 580import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit'; 581 582class Params { 583 text: string = "" 584 585 constructor(text: string) { 586 this.text = text; 587 } 588} 589 590// Custom component 591@Component 592struct TextBuilder { 593 // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute. 594 @Prop message: string = "TextBuilder"; 595 596 build() { 597 Row() { 598 Column() { 599 Text(this.message) 600 .fontSize(50) 601 .fontWeight(FontWeight.Bold) 602 .margin({ bottom: 36 }) 603 .fontColor($r(`app.color.text_color`)) 604 .backgroundColor($r(`app.color.start_window_background`)) 605 } 606 } 607 } 608} 609 610@Builder 611function buildText(params: Params) { 612 Column() { 613 Text(params.text) 614 .fontSize(50) 615 .fontWeight(FontWeight.Bold) 616 .margin({ bottom: 36 }) 617 .fontColor($r(`app.color.text_color`)) 618 TextBuilder({ message: params.text }) // Custom component 619 }.backgroundColor($r(`app.color.start_window_background`)) 620} 621 622class TextNodeController extends NodeController { 623 private textNode: BuilderNode<[Params]> | null = null; 624 private message: string = ""; 625 626 constructor(message: string) { 627 super() 628 this.message = message; 629 } 630 631 makeNode(context: UIContext): FrameNode | null { 632 return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null; 633 } 634 635 createNode(context: UIContext) { 636 this.textNode = new BuilderNode(context); 637 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)); 638 builderNodeMap.push(this.textNode); 639 } 640 641 deleteNode() { 642 let node = builderNodeMap.pop(); 643 node?.dispose(); 644 } 645 646 update(message: string) { 647 if (this.textNode !== null) { 648 // Call update to perform an update. 649 this.textNode.update(new Params(message)); 650 } 651 } 652} 653 654// Record the created custom node object. 655const builderNodeMap: Array<BuilderNode<[Params]>> = new Array(); 656 657function updateColorMode() { 658 builderNodeMap.forEach((value, index) => { 659 // Notify BuilderNode of the environment changes. 660 value.updateConfiguration(); 661 }) 662} 663 664@Entry 665@Component 666struct Index { 667 @State message: string = "hello" 668 private textNodeController: TextNodeController = new TextNodeController(this.message); 669 private count = 0; 670 671 aboutToAppear(): void { 672 let environmentCallback: EnvironmentCallback = { 673 onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => { 674 console.log('onMemoryLevel'); 675 }, 676 onConfigurationUpdated: (config: Configuration): void => { 677 console.log('onConfigurationUpdated ' + JSON.stringify(config)); 678 updateColorMode(); 679 } 680 } 681 // Register a callback. 682 this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback); 683 // Create a custom node and add it to the map. 684 this.textNodeController.createNode(this.getUIContext()); 685 } 686 687 aboutToDisappear(): void { 688 // Remove the reference to the custom node from the map and release the node. 689 this.textNodeController.deleteNode(); 690 } 691 692 build() { 693 Row() { 694 Column() { 695 NodeContainer(this.textNodeController) 696 .width('100%') 697 .height(200) 698 .backgroundColor('#FFF0F0F0') 699 Button('Update') 700 .onClick(() => { 701 this.count += 1; 702 const message = "Update " + this.count.toString(); 703 this.textNodeController.update(message); 704 }) 705 } 706 .width('100%') 707 .height('100%') 708 } 709 .height('100%') 710 } 711} 712``` 713 714## Cross-Page Reuse Considerations 715 716With use of [routing](../reference/apis-arkui/js-apis-router.md) APIs such as [router.replaceUrl](../reference/apis-arkui/js-apis-router.md#routerreplaceurl9), [router.back](../reference/apis-arkui/js-apis-router.md#routerback), [router.clear](../reference/apis-arkui/js-apis-router.md#routerclear), and [router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10) to navigate between pages, issues may arise when you reuse a cached BuilderNode from a page that is about to be destroyed. Specifically, the reused BuilderNode might not update its data correctly, or newly created nodes might not display as expected. For example, when you use [router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10), consider the following scenario: When the **router replace** button is clicked, the page switches to PageTwo, and the flag **isShowText** is set to **false**. 717 718```ts 719// ets/pages/Index.ets 720import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 721import "ets/pages/PageTwo" 722 723@Builder 724function buildText() { 725 // Use syntax nodes to generate a BuilderProxyNode within @Builder. 726 if (true) { 727 MyComponent() 728 } 729} 730 731@Component 732struct MyComponent { 733 @StorageLink("isShowText") isShowText: boolean = true; 734 735 build() { 736 if (this.isShowText) { 737 Column() { 738 Text("BuilderNode Reuse") 739 .fontSize(36) 740 .fontWeight(FontWeight.Bold) 741 .padding(16) 742 } 743 } 744 } 745} 746 747class TextNodeController extends NodeController { 748 private rootNode: FrameNode | null = null; 749 private textNode: BuilderNode<[]> | null = null; 750 751 makeNode(context: UIContext): FrameNode | null { 752 this.rootNode = new FrameNode(context); 753 754 if (AppStorage.has("textNode")) { 755 // Reuse the BuilderNode from AppStorage. 756 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 757 const parent = this.textNode.getFrameNode()?.getParent(); 758 if (parent) { 759 parent.removeChild(this.textNode.getFrameNode()); 760 } 761 } else { 762 this.textNode = new BuilderNode(context); 763 this.textNode.build(wrapBuilder<[]>(buildText)); 764 // Save the created BuilderNode to AppStorage. 765 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 766 } 767 this.rootNode.appendChild(this.textNode.getFrameNode()); 768 769 return this.rootNode; 770 } 771} 772 773@Entry({ routeName: "myIndex" }) 774@Component 775struct Index { 776 aboutToAppear(): void { 777 AppStorage.setOrCreate<boolean>("isShowText", true); 778 } 779 780 build() { 781 Row() { 782 Column() { 783 NodeContainer(new TextNodeController()) 784 .width('100%') 785 .backgroundColor('#FFF0F0F0') 786 Button('Router pageTwo') 787 .onClick(() => { 788 // Change the state variable in AppStorage to trigger re-creation of the Text node. 789 AppStorage.setOrCreate<boolean>("isShowText", false); 790 791 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 792 }) 793 .margin({ top: 16 }) 794 } 795 .width('100%') 796 .height('100%') 797 .padding(16) 798 } 799 .height('100%') 800 } 801} 802``` 803 804The implementation of **PageTwo** is as follows: 805 806```ts 807// ets/pages/PageTwo.ets 808// This page contains a button to navigate back to the home page, where the original text disappears. 809import "ets/pages/Index" 810 811@Entry({ routeName: "pageTwo" }) 812@Component 813struct PageTwo { 814 build() { 815 Column() { 816 Button('Router replace to index') 817 .onClick(() => { 818 this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" }); 819 }) 820 } 821 .height('100%') 822 .width('100%') 823 .alignItems(HorizontalAlign.Center) 824 .padding(16) 825 } 826} 827``` 828 829 830 831In versions earlier than API version 16, you need to manually remove the BuilderNode from the cache, AppStorage in this example, when the page is destroyed. 832 833Since API version 16, the BuilderNode automatically refreshes its content when reused in a new page. This means you no longer need to remove the BuilderNode from the cache when the page is destroyed. 834 835```ts 836// ets/pages/Index.ets 837import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 838import "ets/pages/PageTwo" 839 840@Builder 841function buildText() { 842 // Use syntax nodes to generate a BuilderProxyNode within @Builder. 843 if (true) { 844 MyComponent() 845 } 846} 847 848@Component 849struct MyComponent { 850 @StorageLink("isShowText") isShowText: boolean = true; 851 852 build() { 853 if (this.isShowText) { 854 Column() { 855 Text("BuilderNode Reuse") 856 .fontSize(36) 857 .fontWeight(FontWeight.Bold) 858 .padding(16) 859 } 860 } 861 } 862} 863 864class TextNodeController extends NodeController { 865 private rootNode: FrameNode | null = null; 866 private textNode: BuilderNode<[]> | null = null; 867 868 makeNode(context: UIContext): FrameNode | null { 869 this.rootNode = new FrameNode(context); 870 871 if (AppStorage.has("textNode")) { 872 // Reuse the BuilderNode from AppStorage. 873 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 874 const parent = this.textNode.getFrameNode()?.getParent(); 875 if (parent) { 876 parent.removeChild(this.textNode.getFrameNode()); 877 } 878 } else { 879 this.textNode = new BuilderNode(context); 880 this.textNode.build(wrapBuilder<[]>(buildText)); 881 // Save the created BuilderNode to AppStorage. 882 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 883 } 884 this.rootNode.appendChild(this.textNode.getFrameNode()); 885 886 return this.rootNode; 887 } 888} 889 890@Entry({ routeName: "myIndex" }) 891@Component 892struct Index { 893 aboutToAppear(): void { 894 AppStorage.setOrCreate<boolean>("isShowText", true); 895 } 896 897 build() { 898 Row() { 899 Column() { 900 NodeContainer(new TextNodeController()) 901 .width('100%') 902 .backgroundColor('#FFF0F0F0') 903 Button('Router pageTwo') 904 .onClick(() => { 905 // Change the state variable in AppStorage to trigger re-creation of the Text node. 906 AppStorage.setOrCreate<boolean>("isShowText", false); 907 // Remove the BuilderNode from AppStorage. 908 AppStorage.delete("textNode"); 909 910 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 911 }) 912 .margin({ top: 16 }) 913 } 914 .width('100%') 915 .height('100%') 916 .padding(16) 917 } 918 .height('100%') 919 } 920} 921``` 922 923 924## Using the LocalStorage in the BuilderNode 925 926Since API version 12, custom components can receive [LocalStorage](../quick-start/arkts-localstorage.md) instances. You can use LocalStorage related decorators such as [@LocalStorageProp](../quick-start/arkts-localstorage.md#localstorageprop) and [@LocalStorageLink](../quick-start/arkts-localstorage.md#localstoragelink) by [passing LocalStorage instances](../quick-start/arkts-localstorage.md#example-of-providing-a-custom-component-with-access-to-a-localstorage-instance). 927 928```ts 929import { BuilderNode, NodeController, UIContext } from '@kit.ArkUI'; 930 931let localStorage1: LocalStorage = new LocalStorage(); 932localStorage1.setOrCreate('PropA', 'PropA'); 933 934let localStorage2: LocalStorage = new LocalStorage(); 935localStorage2.setOrCreate('PropB', 'PropB'); 936 937@Entry(localStorage1) 938@Component 939struct Index { 940 // PropA is in two-way synchronization with PropA in localStorage1. 941 @LocalStorageLink('PropA') PropA: string = 'Hello World'; 942 @State count: number = 0; 943 private controller: NodeController = new MyNodeController(this.count, localStorage2); 944 945 build() { 946 Row() { 947 Column() { 948 Text(this.PropA) 949 .fontSize(50) 950 .fontWeight(FontWeight.Bold) 951 // Use the LocalStorage instance localStorage2. 952 Child({ count: this.count }, localStorage2) 953 NodeContainer(this.controller) 954 } 955 .width('100%') 956 } 957 .height('100%') 958 } 959} 960 961interface Params { 962 count: number; 963 localStorage: LocalStorage; 964} 965 966@Builder 967function CreateChild(params: Params) { 968 // Pass localStorage during construction. 969 Child({ count: params.count }, params.localStorage) 970} 971 972class MyNodeController extends NodeController { 973 private count?: number; 974 private localStorage ?: LocalStorage; 975 976 constructor(count: number, localStorage: LocalStorage) { 977 super(); 978 this.count = count; 979 this.localStorage = localStorage; 980 } 981 982 makeNode(uiContext: UIContext): FrameNode | null { 983 let builderNode = new BuilderNode<[Params]>(uiContext); 984 // Pass localStorage during construction. 985 builderNode.build(wrapBuilder(CreateChild), { count: this.count, localStorage: this.localStorage }); 986 return builderNode.getFrameNode(); 987 } 988} 989 990@Component 991struct Child { 992 @Prop count: number; 993 // 'Hello World' is in two-way synchronization with PropB in localStorage2. If there is no PropB in localStorage2, the default value 'Hello World' is used. 994 @LocalStorageLink('PropB') PropB: string = 'Hello World'; 995 996 build() { 997 Text(this.PropB) 998 .fontSize(50) 999 .fontWeight(FontWeight.Bold) 1000 } 1001} 1002``` 1003