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 201- Use the [updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12) API to trigger a full update of all nodes within the BuilderNode. 202 203 204 205```ts 206import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 207 208class Params { 209 text: string = ""; 210 constructor(text: string) { 211 this.text = text; 212 } 213} 214 215// Custom component 216@Component 217struct TextBuilder { 218 // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute. 219 @Prop message: string = "TextBuilder"; 220 221 build() { 222 Row() { 223 Column() { 224 Text(this.message) 225 .fontSize(50) 226 .fontWeight(FontWeight.Bold) 227 .margin({ bottom: 36 }) 228 .backgroundColor(Color.Gray) 229 } 230 } 231 } 232} 233 234@Builder 235function buildText(params: Params) { 236 Column() { 237 Text(params.text) 238 .fontSize(50) 239 .fontWeight(FontWeight.Bold) 240 .margin({ bottom: 36 }) 241 TextBuilder({ message: params.text }) // Custom component 242 } 243} 244 245class TextNodeController extends NodeController { 246 private textNode: BuilderNode<[Params]> | null = null; 247 private message: string = ""; 248 249 constructor(message: string) { 250 super() 251 this.message = message 252 } 253 254 makeNode(context: UIContext): FrameNode | null { 255 this.textNode = new BuilderNode(context); 256 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)) 257 return this.textNode.getFrameNode(); 258 } 259 260 update(message: string) { 261 if (this.textNode !== null) { 262 // Call update to perform an update. 263 this.textNode.update(new Params(message)); 264 } 265 } 266} 267 268@Entry 269@Component 270struct Index { 271 @State message: string = "hello"; 272 private textNodeController: TextNodeController = new TextNodeController(this.message); 273 private count = 0; 274 275 build() { 276 Row() { 277 Column() { 278 NodeContainer(this.textNodeController) 279 .width('100%') 280 .height(200) 281 .backgroundColor('#FFF0F0F0') 282 Button('Update') 283 .onClick(() => { 284 this.count += 1; 285 const message = "Update " + this.count.toString(); 286 this.textNodeController.update(message); 287 }) 288 } 289 .width('100%') 290 .height('100%') 291 } 292 .height('100%') 293 } 294} 295``` 296 297## Canceling the Reference to the Entity Node 298 299A **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. 300 301> **NOTE** 302> 303> 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. 304 305## Injecting a Touch Event 306 307Use 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. 308 309 310 311The 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. 312 313```ts 314import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI'; 315 316class Params { 317 text: string = "this is a text"; 318} 319 320@Builder 321function ButtonBuilder(params: Params) { 322 Column() { 323 Button(`button ` + params.text) 324 .borderWidth(2) 325 .backgroundColor(Color.Orange) 326 .width("100%") 327 .height("100%") 328 .gesture( 329 TapGesture() 330 .onAction((event: GestureEvent) => { 331 console.log("TapGesture"); 332 }) 333 ) 334 } 335 .width(500) 336 .height(300) 337 .backgroundColor(Color.Gray) 338} 339 340class MyNodeController extends NodeController { 341 private rootNode: BuilderNode<[Params]> | null = null; 342 private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder); 343 344 makeNode(uiContext: UIContext): FrameNode | null { 345 this.rootNode = new BuilderNode(uiContext); 346 this.rootNode.build(this.wrapBuilder, { text: "this is a string" }) 347 return this.rootNode.getFrameNode(); 348 } 349 350 postTouchEvent(touchEvent: TouchEvent): void { 351 if (this.rootNode == null) { 352 return; 353 } 354 let result = this.rootNode.postTouchEvent(touchEvent); 355 console.log("result " + result); 356 } 357} 358 359@Entry 360@Component 361struct MyComponent { 362 private nodeController: MyNodeController = new MyNodeController(); 363 364 build() { 365 Column() { 366 NodeContainer(this.nodeController) 367 .height(300) 368 .width(500) 369 Column() 370 .width(500) 371 .height(300) 372 .backgroundColor(Color.Pink) 373 .onTouch((event) => { 374 if (event != undefined) { 375 this.nodeController.postTouchEvent(event); 376 } 377 }) 378 } 379 } 380} 381``` 382 383## Reusing a BuilderNode 384 385To 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. 386 387```ts 388import { FrameNode,NodeController,BuilderNode,UIContext } from "@kit.ArkUI"; 389 390class MyDataSource { 391 private dataArray: string[] = []; 392 private listener: DataChangeListener | null = null 393 394 public totalCount(): number { 395 return this.dataArray.length; 396 } 397 398 public getData(index: number) { 399 return this.dataArray[index]; 400 } 401 402 public pushData(data: string) { 403 this.dataArray.push(data); 404 } 405 406 public reloadListener(): void { 407 this.listener?.onDataReloaded(); 408 } 409 410 public registerDataChangeListener(listener: DataChangeListener): void { 411 this.listener = listener; 412 } 413 414 public unregisterDataChangeListener(): void { 415 this.listener = null; 416 } 417} 418 419class Params { 420 item: string = ''; 421 422 constructor(item: string) { 423 this.item = item; 424 } 425} 426 427@Builder 428function buildNode(param: Params = new Params("hello")) { 429 ReusableChildComponent2({ item: param.item }); 430} 431 432class MyNodeController extends NodeController { 433 public builderNode: BuilderNode<[Params]> | null = null; 434 public item: string = ""; 435 436 makeNode(uiContext: UIContext): FrameNode | null { 437 if (this.builderNode == null) { 438 this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } }); 439 this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item)); 440 } 441 return this.builderNode.getFrameNode(); 442 } 443} 444 445@Reusable 446@Component 447struct ReusableChildComponent { 448 @State item: string = ''; 449 private controller: MyNodeController = new MyNodeController(); 450 451 aboutToAppear() { 452 this.controller.item = this.item; 453 } 454 455 aboutToRecycle(): void { 456 console.log("ReusableChildComponent aboutToRecycle " + this.item); 457 this.controller?.builderNode?.recycle(); 458 } 459 460 aboutToReuse(params: object): void { 461 console.log("ReusableChildComponent aboutToReuse " + JSON.stringify(params)); 462 this.controller?.builderNode?.reuse(params); 463 } 464 465 build() { 466 NodeContainer(this.controller); 467 } 468} 469 470@Component 471struct ReusableChildComponent2 { 472 @Prop item: string = "false"; 473 474 aboutToReuse(params: Record<string, object>) { 475 console.log("ReusableChildComponent2 Reusable 2 " + JSON.stringify(params)); 476 } 477 478 aboutToRecycle(): void { 479 console.log("ReusableChildComponent2 aboutToRecycle 2 " + this.item); 480 } 481 482 build() { 483 Row() { 484 Text(this.item) 485 .fontSize(20) 486 .backgroundColor(Color.Yellow) 487 .margin({ left: 10 }) 488 }.margin({ left: 10, right: 10 }) 489 } 490} 491 492 493@Entry 494@Component 495struct Index { 496 @State data: MyDataSource = new MyDataSource(); 497 498 aboutToAppear() { 499 for (let i = 0;i < 100; i++) { 500 this.data.pushData(i.toString()); 501 } 502 } 503 504 build() { 505 Column() { 506 List({ space: 3 }) { 507 LazyForEach(this.data, (item: string) => { 508 ListItem() { 509 ReusableChildComponent({ item: item }) 510 } 511 }, (item: string) => item) 512 } 513 .width('100%') 514 .height('100%') 515 } 516 } 517} 518``` 519 520## Updating Nodes Based on System Environment Changes 521 522Use 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. 523 524> **NOTE** 525> 526> 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. 527 528```ts 529import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 530import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit'; 531 532class Params { 533 text: string = "" 534 535 constructor(text: string) { 536 this.text = text; 537 } 538} 539 540// Custom component 541@Component 542struct TextBuilder { 543 // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute. 544 @Prop message: string = "TextBuilder"; 545 546 build() { 547 Row() { 548 Column() { 549 Text(this.message) 550 .fontSize(50) 551 .fontWeight(FontWeight.Bold) 552 .margin({ bottom: 36 }) 553 .fontColor($r(`app.color.text_color`)) 554 .backgroundColor($r(`app.color.start_window_background`)) 555 } 556 } 557 } 558} 559 560@Builder 561function buildText(params: Params) { 562 Column() { 563 Text(params.text) 564 .fontSize(50) 565 .fontWeight(FontWeight.Bold) 566 .margin({ bottom: 36 }) 567 .fontColor($r(`app.color.text_color`)) 568 TextBuilder({ message: params.text }) // Custom component 569 }.backgroundColor($r(`app.color.start_window_background`)) 570} 571 572class TextNodeController extends NodeController { 573 private textNode: BuilderNode<[Params]> | null = null; 574 private message: string = ""; 575 576 constructor(message: string) { 577 super() 578 this.message = message; 579 } 580 581 makeNode(context: UIContext): FrameNode | null { 582 return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null; 583 } 584 585 createNode(context: UIContext) { 586 this.textNode = new BuilderNode(context); 587 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)); 588 builderNodeMap.push(this.textNode); 589 } 590 591 deleteNode() { 592 let node = builderNodeMap.pop(); 593 node?.dispose(); 594 } 595 596 update(message: string) { 597 if (this.textNode !== null) { 598 // Call update to perform an update. 599 this.textNode.update(new Params(message)); 600 } 601 } 602} 603 604// Record the created custom node object. 605const builderNodeMap: Array<BuilderNode<[Params]>> = new Array(); 606 607function updateColorMode() { 608 builderNodeMap.forEach((value, index) => { 609 // Notify BuilderNode of the environment changes. 610 value.updateConfiguration(); 611 }) 612} 613 614@Entry 615@Component 616struct Index { 617 @State message: string = "hello" 618 private textNodeController: TextNodeController = new TextNodeController(this.message); 619 private count = 0; 620 621 aboutToAppear(): void { 622 let environmentCallback: EnvironmentCallback = { 623 onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => { 624 console.log('onMemoryLevel'); 625 }, 626 onConfigurationUpdated: (config: Configuration): void => { 627 console.log('onConfigurationUpdated ' + JSON.stringify(config)); 628 updateColorMode(); 629 } 630 } 631 // Register a callback. 632 this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback); 633 // Create a custom node and add it to the map. 634 this.textNodeController.createNode(this.getUIContext()); 635 } 636 637 aboutToDisappear(): void { 638 // Remove the reference to the custom node from the map and release the node. 639 this.textNodeController.deleteNode(); 640 } 641 642 build() { 643 Row() { 644 Column() { 645 NodeContainer(this.textNodeController) 646 .width('100%') 647 .height(200) 648 .backgroundColor('#FFF0F0F0') 649 Button('Update') 650 .onClick(() => { 651 this.count += 1; 652 const message = "Update " + this.count.toString(); 653 this.textNodeController.update(message); 654 }) 655 } 656 .width('100%') 657 .height('100%') 658 } 659 .height('100%') 660 } 661} 662``` 663 664## Cross-Page Reuse Considerations 665 666With 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**. 667 668```ts 669// ets/pages/Index.ets 670import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 671import "ets/pages/PageTwo" 672 673@Builder 674function buildText() { 675 // Use syntax nodes to generate a BuilderProxyNode within @Builder. 676 if (true) { 677 MyComponent() 678 } 679} 680 681@Component 682struct MyComponent { 683 @StorageLink("isShowText") isShowText: boolean = true; 684 685 build() { 686 if (this.isShowText) { 687 Column() { 688 Text("BuilderNode Reuse") 689 .fontSize(36) 690 .fontWeight(FontWeight.Bold) 691 .padding(16) 692 } 693 } 694 } 695} 696 697class TextNodeController extends NodeController { 698 private rootNode: FrameNode | null = null; 699 private textNode: BuilderNode<[]> | null = null; 700 701 makeNode(context: UIContext): FrameNode | null { 702 this.rootNode = new FrameNode(context); 703 704 if (AppStorage.has("textNode")) { 705 // Reuse the BuilderNode from AppStorage. 706 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 707 const parent = this.textNode.getFrameNode()?.getParent(); 708 if (parent) { 709 parent.removeChild(this.textNode.getFrameNode()); 710 } 711 } else { 712 this.textNode = new BuilderNode(context); 713 this.textNode.build(wrapBuilder<[]>(buildText)); 714 // Save the created BuilderNode to AppStorage. 715 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 716 } 717 this.rootNode.appendChild(this.textNode.getFrameNode()); 718 719 return this.rootNode; 720 } 721} 722 723@Entry({ routeName: "myIndex" }) 724@Component 725struct Index { 726 aboutToAppear(): void { 727 AppStorage.setOrCreate<boolean>("isShowText", true); 728 } 729 730 build() { 731 Row() { 732 Column() { 733 NodeContainer(new TextNodeController()) 734 .width('100%') 735 .backgroundColor('#FFF0F0F0') 736 Button('Router pageTwo') 737 .onClick(() => { 738 // Change the state variable in AppStorage to trigger re-creation of the Text node. 739 AppStorage.setOrCreate<boolean>("isShowText", false); 740 741 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 742 }) 743 .margin({ top: 16 }) 744 } 745 .width('100%') 746 .height('100%') 747 .padding(16) 748 } 749 .height('100%') 750 } 751} 752``` 753 754The implementation of **PageTwo** is as follows: 755 756```ts 757// ets/pages/PageTwo.ets 758// This page contains a button to navigate back to the home page, where the original text disappears. 759import "ets/pages/Index" 760 761@Entry({ routeName: "pageTwo" }) 762@Component 763struct PageTwo { 764 build() { 765 Column() { 766 Button('Router replace to index') 767 .onClick(() => { 768 this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" }); 769 }) 770 } 771 .height('100%') 772 .width('100%') 773 .alignItems(HorizontalAlign.Center) 774 .padding(16) 775 } 776} 777``` 778 779 780 781In 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. 782 783Since 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. 784 785```ts 786// ets/pages/Index.ets 787import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 788import "ets/pages/PageTwo" 789 790@Builder 791function buildText() { 792 // Use syntax nodes to generate a BuilderProxyNode within @Builder. 793 if (true) { 794 MyComponent() 795 } 796} 797 798@Component 799struct MyComponent { 800 @StorageLink("isShowText") isShowText: boolean = true; 801 802 build() { 803 if (this.isShowText) { 804 Column() { 805 Text("BuilderNode Reuse") 806 .fontSize(36) 807 .fontWeight(FontWeight.Bold) 808 .padding(16) 809 } 810 } 811 } 812} 813 814class TextNodeController extends NodeController { 815 private rootNode: FrameNode | null = null; 816 private textNode: BuilderNode<[]> | null = null; 817 818 makeNode(context: UIContext): FrameNode | null { 819 this.rootNode = new FrameNode(context); 820 821 if (AppStorage.has("textNode")) { 822 // Reuse the BuilderNode from AppStorage. 823 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 824 const parent = this.textNode.getFrameNode()?.getParent(); 825 if (parent) { 826 parent.removeChild(this.textNode.getFrameNode()); 827 } 828 } else { 829 this.textNode = new BuilderNode(context); 830 this.textNode.build(wrapBuilder<[]>(buildText)); 831 // Save the created BuilderNode to AppStorage. 832 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 833 } 834 this.rootNode.appendChild(this.textNode.getFrameNode()); 835 836 return this.rootNode; 837 } 838} 839 840@Entry({ routeName: "myIndex" }) 841@Component 842struct Index { 843 aboutToAppear(): void { 844 AppStorage.setOrCreate<boolean>("isShowText", true); 845 } 846 847 build() { 848 Row() { 849 Column() { 850 NodeContainer(new TextNodeController()) 851 .width('100%') 852 .backgroundColor('#FFF0F0F0') 853 Button('Router pageTwo') 854 .onClick(() => { 855 // Change the state variable in AppStorage to trigger re-creation of the Text node. 856 AppStorage.setOrCreate<boolean>("isShowText", false); 857 // Remove the BuilderNode from AppStorage. 858 AppStorage.delete("textNode"); 859 860 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 861 }) 862 .margin({ top: 16 }) 863 } 864 .width('100%') 865 .height('100%') 866 .padding(16) 867 } 868 .height('100%') 869 } 870} 871``` 872