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](../ui/state-management/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 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/arkts-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](../ui/state-management/arkts-builder.md#global-custom-builder-function) decorated by @Builder and wrapped by [wrapBuilder](../ui/state-management/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](../ui/state-management/arkts-wrapBuilder.md) used in the **build** method later on. Mismatches can cause compilation warnings and failures. 38 39## Creating a Component Tree 40 41Use the **build** API of **BuilderNode** to create a 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 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 a Component Tree 192 193The **build** API of a **BuilderNode** object constructs a component tree by accepting a **WrappedBuilder** object and maintains a reference to the root node of the created component tree. 194 195Custom component updates follow the update mechanisms of [state management](../ui/state-management/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](../ui/state-management/arkts-prop.md) or [\@ObjectLink](../ui/state-management/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 **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## Implementing Node Reuse with the BuilderNode reuse and recycle APIs 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 388In the following example, the custom component **ReusableChildComponent** can pass reuse and recycle events to its nested custom component **ChildComponent3**. However, these events cannot automatically reach another custom component, **ChildComponent2**, as it is separated by a BuilderNode. To enable reuse for **ChildComponent2**, you must explicitly call the **reuse** and **recycle** APIs on the BuilderNode to forward these events to the component. 389 390 391 392 393```ts 394import { FrameNode, NodeController, BuilderNode, UIContext } from "@kit.ArkUI"; 395 396const TEST_TAG: string = "Reuse+Recycle"; 397 398class MyDataSource { 399 private dataArray: string[] = []; 400 private listener: DataChangeListener | null = null 401 402 public totalCount(): number { 403 return this.dataArray.length; 404 } 405 406 public getData(index: number) { 407 return this.dataArray[index]; 408 } 409 410 public pushData(data: string) { 411 this.dataArray.push(data); 412 } 413 414 public reloadListener(): void { 415 this.listener?.onDataReloaded(); 416 } 417 418 public registerDataChangeListener(listener: DataChangeListener): void { 419 this.listener = listener; 420 } 421 422 public unregisterDataChangeListener(): void { 423 this.listener = null; 424 } 425} 426 427class Params { 428 item: string = ''; 429 430 constructor(item: string) { 431 this.item = item; 432 } 433} 434 435@Builder 436function buildNode(param: Params = new Params("hello")) { 437 Row() { 438 Text(`C${param.item} -- `) 439 ChildComponent2({ item: param.item }) // This custom component cannot be correctly reused in the BuilderNode. 440 } 441} 442 443class MyNodeController extends NodeController { 444 public builderNode: BuilderNode<[Params]> | null = null; 445 public item: string = ""; 446 447 makeNode(uiContext: UIContext): FrameNode | null { 448 if (this.builderNode == null) { 449 this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } }); 450 this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item)); 451 } 452 return this.builderNode.getFrameNode(); 453 } 454} 455 456// The custom component that is reused and recycled will have its state variables updated, and the state variables of the nested ChildComponent3 will also be updated. However, the BuilderNode will block this propagation process. 457@Reusable 458@Component 459struct ReusableChildComponent { 460 @Prop item: string = ''; 461 @Prop switch: string = ''; 462 private controller: MyNodeController = new MyNodeController(); 463 464 aboutToAppear() { 465 this.controller.item = this.item; 466 } 467 468 aboutToRecycle(): void { 469 console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`); 470 471 // When the switch is open, pass the recycle event to the nested custom component, such as ChildComponent2, through the BuilderNode's recycle API to complete recycling. 472 if (this.switch === 'open') { 473 this.controller?.builderNode?.recycle(); 474 } 475 } 476 477 aboutToReuse(params: object): void { 478 console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`); 479 480 // When the switch is open, pass the reuse event to the nested custom component, such as ChildComponent2, through the BuilderNode's reuse API to complete reuse. 481 if (this.switch === 'open') { 482 this.controller?.builderNode?.reuse(params); 483 } 484 } 485 486 build() { 487 Row() { 488 Text(`A${this.item}--`) 489 ChildComponent3({ item: this.item }) 490 NodeContainer(this.controller); 491 } 492 } 493} 494 495@Component 496struct ChildComponent2 { 497 @Prop item: string = "false"; 498 499 aboutToReuse(params: Record<string, object>) { 500 console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`); 501 } 502 503 aboutToRecycle(): void { 504 console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`); 505 } 506 507 build() { 508 Row() { 509 Text(`D${this.item}`) 510 .fontSize(20) 511 .backgroundColor(Color.Yellow) 512 .margin({ left: 10 }) 513 }.margin({ left: 10, right: 10 }) 514 } 515} 516 517@Component 518struct ChildComponent3 { 519 @Prop item: string = "false"; 520 521 aboutToReuse(params: Record<string, object>) { 522 console.log(`${TEST_TAG} ChildComponent3 aboutToReuse ${JSON.stringify(params)}`); 523 } 524 525 aboutToRecycle(): void { 526 console.log(`${TEST_TAG} ChildComponent3 aboutToRecycle ${this.item}`); 527 } 528 529 build() { 530 Row() { 531 Text(`B${this.item}`) 532 .fontSize(20) 533 .backgroundColor(Color.Yellow) 534 .margin({ left: 10 }) 535 }.margin({ left: 10, right: 10 }) 536 } 537} 538 539 540@Entry 541@Component 542struct Index { 543 @State data: MyDataSource = new MyDataSource(); 544 545 aboutToAppear() { 546 for (let i = 0; i < 100; i++) { 547 this.data.pushData(i.toString()); 548 } 549 } 550 551 build() { 552 Column() { 553 List({ space: 3 }) { 554 LazyForEach(this.data, (item: string) => { 555 ListItem() { 556 ReusableChildComponent({ 557 item: item, 558 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. 559 }) 560 } 561 }, (item: string) => item) 562 } 563 .width('100%') 564 .height('100%') 565 } 566 } 567} 568``` 569 570 571## Using the @Reusable Decorator with BuilderNode Child Components 572 573The reuse mechanism of **BuilderNode** is fundamentally incompatible with the component reuse behavior enabled by the [@Reusable](./state-management/arkts-reusable.md) decorator. Therefore, when a BuilderNode contains a custom component as its child nodes, that component cannot be decorated with @Reusable. Attempting to do so will trigger a JS crash. To use the @Reusable decorator, first wrap the target custom component with a regular custom component. 574 575In the following example, when **ReusableChildComponent** serves as a direct child of the BuilderNode, it cannot be decorated with @Reusable. By wrapping it with **ChildComponent2**, **ReusableChildComponent** can then safely use the @Reusable decorator. 576 577 578 579```ts 580import { FrameNode, NodeController, BuilderNode, UIContext } from '@kit.ArkUI'; 581 582const TEST_TAG: string = "Reusable"; 583 584class Params { 585 item: string = ''; 586 587 constructor(item: string) { 588 this.item = item; 589 } 590} 591 592@Builder 593function buildNode(param: Params = new Params("Hello")) { 594 ChildComponent2({ item: param.item }) 595 // If ReusableChildComponent is used directly, a compilation error is reported. 596 // ReusableChildComponent({ item: param.item }) 597} 598 599class MyNodeController extends NodeController { 600 public builderNode: BuilderNode<[Params]> | null = null; 601 public item: string = ""; 602 603 constructor(item: string) { 604 super(); 605 this.item = item; 606 } 607 608 makeNode(uiContext: UIContext): FrameNode | null { 609 if (this.builderNode == null) { 610 this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } }); 611 this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item)); 612 } 613 return this.builderNode.getFrameNode(); 614 } 615} 616 617// This custom component is decorated with @Reusable and therefore cannot be directly mounted as a child node of the BuilderNode. 618@Reusable 619@Component 620struct ReusableChildComponent { 621 @Prop item: string = ''; 622 623 aboutToReuse(params: object): void { 624 console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`); 625 } 626 627 aboutToRecycle(): void { 628 console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`); 629 } 630 631 build() { 632 Text(`A--${this.item}`) 633 } 634} 635 636// Custom component not decorated with @Reusable 637@Component 638struct ChildComponent2 { 639 @Prop item: string = ""; 640 641 aboutToReuse(params: Record<string, object>) { 642 console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`); 643 } 644 645 aboutToRecycle(): void { 646 console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`); 647 } 648 649 build() { 650 ReusableChildComponent({ item: this.item }) 651 } 652} 653 654 655@Entry 656@Component 657struct Index { 658 @State controller: MyNodeController = new MyNodeController("Child"); 659 660 build() { 661 Column() { 662 NodeContainer(this.controller) 663 } 664 .width('100%') 665 .height('100%') 666 } 667} 668``` 669 670## Updating Nodes Based on System Environment Changes 671 672Use 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. 673 674> **NOTE** 675> 676> 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. 677 678```ts 679import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 680import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit'; 681 682class Params { 683 text: string = "" 684 685 constructor(text: string) { 686 this.text = text; 687 } 688} 689 690// Custom component 691@Component 692struct TextBuilder { 693 // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute. 694 @Prop message: string = "TextBuilder"; 695 696 build() { 697 Row() { 698 Column() { 699 Text(this.message) 700 .fontSize(50) 701 .fontWeight(FontWeight.Bold) 702 .margin({ bottom: 36 }) 703 .fontColor($r(`app.color.text_color`)) 704 .backgroundColor($r(`app.color.start_window_background`)) 705 } 706 } 707 } 708} 709 710@Builder 711function buildText(params: Params) { 712 Column() { 713 Text(params.text) 714 .fontSize(50) 715 .fontWeight(FontWeight.Bold) 716 .margin({ bottom: 36 }) 717 .fontColor($r(`app.color.text_color`)) 718 TextBuilder({ message: params.text }) // Custom component 719 }.backgroundColor($r(`app.color.start_window_background`)) 720} 721 722class TextNodeController extends NodeController { 723 private textNode: BuilderNode<[Params]> | null = null; 724 private message: string = ""; 725 726 constructor(message: string) { 727 super() 728 this.message = message; 729 } 730 731 makeNode(context: UIContext): FrameNode | null { 732 return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null; 733 } 734 735 createNode(context: UIContext) { 736 this.textNode = new BuilderNode(context); 737 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)); 738 builderNodeMap.push(this.textNode); 739 } 740 741 deleteNode() { 742 let node = builderNodeMap.pop(); 743 node?.dispose(); 744 } 745 746 update(message: string) { 747 if (this.textNode !== null) { 748 // Call update to perform an update. 749 this.textNode.update(new Params(message)); 750 } 751 } 752} 753 754// Record the created custom node object. 755const builderNodeMap: Array<BuilderNode<[Params]>> = new Array(); 756 757function updateColorMode() { 758 builderNodeMap.forEach((value, index) => { 759 // Notify BuilderNode of the environment changes. 760 value.updateConfiguration(); 761 }) 762} 763 764@Entry 765@Component 766struct Index { 767 @State message: string = "hello" 768 private textNodeController: TextNodeController = new TextNodeController(this.message); 769 private count = 0; 770 771 aboutToAppear(): void { 772 let environmentCallback: EnvironmentCallback = { 773 onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => { 774 console.log('onMemoryLevel'); 775 }, 776 onConfigurationUpdated: (config: Configuration): void => { 777 console.log('onConfigurationUpdated ' + JSON.stringify(config)); 778 updateColorMode(); 779 } 780 } 781 // Register a callback. 782 this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback); 783 // Create a custom node and add it to the map. 784 this.textNodeController.createNode(this.getUIContext()); 785 } 786 787 aboutToDisappear(): void { 788 // Remove the reference to the custom node from the map and release the node. 789 this.textNodeController.deleteNode(); 790 } 791 792 build() { 793 Row() { 794 Column() { 795 NodeContainer(this.textNodeController) 796 .width('100%') 797 .height(200) 798 .backgroundColor('#FFF0F0F0') 799 Button('Update') 800 .onClick(() => { 801 this.count += 1; 802 const message = "Update " + this.count.toString(); 803 this.textNodeController.update(message); 804 }) 805 } 806 .width('100%') 807 .height('100%') 808 } 809 .height('100%') 810 } 811} 812``` 813 814## Cross-Page Reuse Considerations 815 816With use of [routing](../reference/apis-arkui/arkts-apis-uicontext-router.md) APIs such as [router.replaceUrl](../reference/apis-arkui/arkts-apis-uicontext-router.md#replaceurl), [router.back](../reference/apis-arkui/arkts-apis-uicontext-router.md#back), [router.clear](../reference/apis-arkui/arkts-apis-uicontext-router.md#clear), and [router.replaceNamedRoute](../reference/apis-arkui/arkts-apis-uicontext-router.md#replacenamedroute) 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/arkts-apis-uicontext-router.md#replacenamedroute), consider the following scenario: When the **router replace** button is clicked, the page switches to PageTwo, and the flag **isShowText** is set to **false**. 817 818```ts 819// ets/pages/Index.ets 820import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 821import "ets/pages/PageTwo" 822 823@Builder 824function buildText() { 825 // Use syntax nodes to generate a BuilderProxyNode within @Builder. 826 if (true) { 827 MyComponent() 828 } 829} 830 831@Component 832struct MyComponent { 833 @StorageLink("isShowText") isShowText: boolean = true; 834 835 build() { 836 if (this.isShowText) { 837 Column() { 838 Text("BuilderNode Reuse") 839 .fontSize(36) 840 .fontWeight(FontWeight.Bold) 841 .padding(16) 842 } 843 } 844 } 845} 846 847class TextNodeController extends NodeController { 848 private rootNode: FrameNode | null = null; 849 private textNode: BuilderNode<[]> | null = null; 850 851 makeNode(context: UIContext): FrameNode | null { 852 this.rootNode = new FrameNode(context); 853 854 if (AppStorage.has("textNode")) { 855 // Reuse the BuilderNode from AppStorage. 856 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 857 const parent = this.textNode.getFrameNode()?.getParent(); 858 if (parent) { 859 parent.removeChild(this.textNode.getFrameNode()); 860 } 861 } else { 862 this.textNode = new BuilderNode(context); 863 this.textNode.build(wrapBuilder<[]>(buildText)); 864 // Save the created BuilderNode to AppStorage. 865 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 866 } 867 this.rootNode.appendChild(this.textNode.getFrameNode()); 868 869 return this.rootNode; 870 } 871} 872 873@Entry({ routeName: "myIndex" }) 874@Component 875struct Index { 876 aboutToAppear(): void { 877 AppStorage.setOrCreate<boolean>("isShowText", true); 878 } 879 880 build() { 881 Row() { 882 Column() { 883 NodeContainer(new TextNodeController()) 884 .width('100%') 885 .backgroundColor('#FFF0F0F0') 886 Button('Router pageTwo') 887 .onClick(() => { 888 // Change the state variable in AppStorage to trigger re-creation of the Text node. 889 AppStorage.setOrCreate<boolean>("isShowText", false); 890 891 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 892 }) 893 .margin({ top: 16 }) 894 } 895 .width('100%') 896 .height('100%') 897 .padding(16) 898 } 899 .height('100%') 900 } 901} 902``` 903 904The implementation of **PageTwo** is as follows: 905 906```ts 907// ets/pages/PageTwo.ets 908// This page contains a button to navigate back to the home page, where the original text disappears. 909import "ets/pages/Index" 910 911@Entry({ routeName: "pageTwo" }) 912@Component 913struct PageTwo { 914 build() { 915 Column() { 916 Button('Router replace to index') 917 .onClick(() => { 918 this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" }); 919 }) 920 } 921 .height('100%') 922 .width('100%') 923 .alignItems(HorizontalAlign.Center) 924 .padding(16) 925 } 926} 927``` 928 929 930 931In 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. 932 933Since 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. 934 935```ts 936// ets/pages/Index.ets 937import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 938import "ets/pages/PageTwo" 939 940@Builder 941function buildText() { 942 // Use syntax nodes to generate a BuilderProxyNode within @Builder. 943 if (true) { 944 MyComponent() 945 } 946} 947 948@Component 949struct MyComponent { 950 @StorageLink("isShowText") isShowText: boolean = true; 951 952 build() { 953 if (this.isShowText) { 954 Column() { 955 Text("BuilderNode Reuse") 956 .fontSize(36) 957 .fontWeight(FontWeight.Bold) 958 .padding(16) 959 } 960 } 961 } 962} 963 964class TextNodeController extends NodeController { 965 private rootNode: FrameNode | null = null; 966 private textNode: BuilderNode<[]> | null = null; 967 968 makeNode(context: UIContext): FrameNode | null { 969 this.rootNode = new FrameNode(context); 970 971 if (AppStorage.has("textNode")) { 972 // Reuse the BuilderNode from AppStorage. 973 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 974 const parent = this.textNode.getFrameNode()?.getParent(); 975 if (parent) { 976 parent.removeChild(this.textNode.getFrameNode()); 977 } 978 } else { 979 this.textNode = new BuilderNode(context); 980 this.textNode.build(wrapBuilder<[]>(buildText)); 981 // Save the created BuilderNode to AppStorage. 982 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 983 } 984 this.rootNode.appendChild(this.textNode.getFrameNode()); 985 986 return this.rootNode; 987 } 988} 989 990@Entry({ routeName: "myIndex" }) 991@Component 992struct Index { 993 aboutToAppear(): void { 994 AppStorage.setOrCreate<boolean>("isShowText", true); 995 } 996 997 build() { 998 Row() { 999 Column() { 1000 NodeContainer(new TextNodeController()) 1001 .width('100%') 1002 .backgroundColor('#FFF0F0F0') 1003 Button('Router pageTwo') 1004 .onClick(() => { 1005 // Change the state variable in AppStorage to trigger re-creation of the Text node. 1006 AppStorage.setOrCreate<boolean>("isShowText", false); 1007 // Remove the BuilderNode from AppStorage. 1008 AppStorage.delete("textNode"); 1009 1010 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 1011 }) 1012 .margin({ top: 16 }) 1013 } 1014 .width('100%') 1015 .height('100%') 1016 .padding(16) 1017 } 1018 .height('100%') 1019 } 1020} 1021``` 1022 1023 1024## Using the LocalStorage in the BuilderNode 1025 1026Since API version 12, custom components can receive [LocalStorage](../ui/state-management/arkts-localstorage.md) instances. You can use LocalStorage related decorators such as [@LocalStorageProp](../ui/state-management/arkts-localstorage.md#localstorageprop) and [@LocalStorageLink](../ui/state-management/arkts-localstorage.md#localstoragelink) by [passing LocalStorage instances](../ui/state-management/arkts-localstorage.md#example-of-providing-a-custom-component-with-access-to-a-localstorage-instance). 1027 1028```ts 1029import { BuilderNode, NodeController, UIContext } from '@kit.ArkUI'; 1030 1031let localStorage1: LocalStorage = new LocalStorage(); 1032localStorage1.setOrCreate('PropA', 'PropA'); 1033 1034let localStorage2: LocalStorage = new LocalStorage(); 1035localStorage2.setOrCreate('PropB', 'PropB'); 1036 1037@Entry(localStorage1) 1038@Component 1039struct Index { 1040 // PropA is in two-way synchronization with PropA in localStorage1. 1041 @LocalStorageLink('PropA') PropA: string = 'Hello World'; 1042 @State count: number = 0; 1043 private controller: NodeController = new MyNodeController(this.count, localStorage2); 1044 1045 build() { 1046 Row() { 1047 Column() { 1048 Text(this.PropA) 1049 .fontSize(50) 1050 .fontWeight(FontWeight.Bold) 1051 // Use the LocalStorage instance localStorage2. 1052 Child({ count: this.count }, localStorage2) 1053 NodeContainer(this.controller) 1054 } 1055 .width('100%') 1056 } 1057 .height('100%') 1058 } 1059} 1060 1061interface Params { 1062 count: number; 1063 localStorage: LocalStorage; 1064} 1065 1066@Builder 1067function CreateChild(params: Params) { 1068 // Pass localStorage during construction. 1069 Child({ count: params.count }, params.localStorage) 1070} 1071 1072class MyNodeController extends NodeController { 1073 private count?: number; 1074 private localStorage ?: LocalStorage; 1075 1076 constructor(count: number, localStorage: LocalStorage) { 1077 super(); 1078 this.count = count; 1079 this.localStorage = localStorage; 1080 } 1081 1082 makeNode(uiContext: UIContext): FrameNode | null { 1083 let builderNode = new BuilderNode<[Params]>(uiContext); 1084 // Pass localStorage during construction. 1085 builderNode.build(wrapBuilder(CreateChild), { count: this.count, localStorage: this.localStorage }); 1086 return builderNode.getFrameNode(); 1087 } 1088} 1089 1090@Component 1091struct Child { 1092 @Prop count: number; 1093 // '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. 1094 @LocalStorageLink('PropB') PropB: string = 'Hello World'; 1095 1096 build() { 1097 Text(this.PropB) 1098 .fontSize(50) 1099 .fontWeight(FontWeight.Bold) 1100 } 1101} 1102``` 1103 1104## Checking BuilderNode Reference Status 1105 1106All frontend nodes are bound to corresponding backend entity nodes. After **dispose()** is called, subsequent calls may cause crashes or return default values. 1107 1108Since API version 20, you can use the [isDisposed](../reference/apis-arkui/js-apis-arkui-builderNode.md#isdisposed20) API to check whether a **BuilderNode** object has released its reference to backend entity nodes. This enables validation before node operations to prevent potential risks. 1109 1110```ts 1111import { NodeController, FrameNode, BuilderNode } from '@kit.ArkUI'; 1112 1113@Builder 1114function buildText() { 1115 Text("Test") 1116 .fontSize(20) 1117 .fontWeight(FontWeight.Bold) 1118} 1119 1120class MyNodeController extends NodeController { 1121 private rootNode: FrameNode | null = null; 1122 private builderNode: BuilderNode<[]> | null = null; 1123 1124 makeNode(uiContext: UIContext): FrameNode | null { 1125 this.rootNode = new FrameNode(uiContext); 1126 this.rootNode.commonAttribute.width(100).height(100).backgroundColor(Color.Pink); 1127 this.builderNode = new BuilderNode<[]>(uiContext); 1128 this.builderNode.build(wrapBuilder<[]>(buildText)); 1129 1130 // Mount the BuilderNode. 1131 this.rootNode.appendChild(this.builderNode.getFrameNode()); 1132 return this.rootNode; 1133 } 1134 1135 disposeBuilderNode() { 1136 // Remove the reference relationship between the BuilderNode and backend entity nodes. 1137 this.builderNode?.dispose(); 1138 } 1139 1140 isDisposed() : string { 1141 if (this.builderNode !== null) { 1142 // Check BuilderNode reference status. 1143 if (this.builderNode.isDisposed()) { 1144 return 'builderNode isDisposed is true'; 1145 } 1146 else { 1147 return 'builderNode isDisposed is false'; 1148 } 1149 } 1150 return 'builderNode is null'; 1151 } 1152} 1153 1154@Entry 1155@Component 1156struct Index { 1157 @State text: string = '' 1158 private myNodeController: MyNodeController = new MyNodeController(); 1159 1160 build() { 1161 Column({ space: 4 }) { 1162 NodeContainer(this.myNodeController) 1163 Button('BuilderNode dispose') 1164 .onClick(() => { 1165 this.myNodeController.disposeBuilderNode(); 1166 this.text = ''; 1167 }) 1168 .width(200) 1169 .height(50) 1170 Button('BuilderNode isDisposed') 1171 .onClick(() => { 1172 this.text = this.myNodeController.isDisposed(); 1173 }) 1174 .width(200) 1175 .height(50) 1176 Text(this.text) 1177 .fontSize(25) 1178 } 1179 .width('100%') 1180 .height('100%') 1181 } 1182} 1183``` 1184 1185## Configuring BuilderNode Freeze Inheritance 1186 1187ArkUI supports [custom component freezing](./state-management/arkts-custom-components-freeze.md), which suspends refresh capabilities for inactive components. When frozen, components will not trigger UI re-rendering even if bound state variables change, reducing refresh load in complex UI scenarios. 1188 1189Since API version 20, a BuilderNode can inherit freeze policies from its parent custom component (the first custom component found when traversing up from the BuilderNode) using the [inheritFreezeOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#inheritfreezeoptions20) API. When freeze inheritance is enabled: If the parent component has freezing enabled ([freezeWhenInactive](./state-management/arkts-create-custom-components.md#freezewheninactive11) set to **true**), the BuilderNode will freeze when inactive and thaw when active, and update using cached data upon reactivation. 1190 1191The BuilderNode has its freeze policy updated only during the tree operations listed below. 1192 1193| Class| API| 1194| -------- | -------- | 1195| [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md) | [appendChild](../reference/apis-arkui/js-apis-arkui-frameNode.md#appendchild12), [insertChildAfter](../reference/apis-arkui/js-apis-arkui-frameNode.md#insertchildafter12), [removeChild](../reference/apis-arkui/js-apis-arkui-frameNode.md#removechild12), [clearChildren](../reference/apis-arkui/js-apis-arkui-frameNode.md#clearchildren12), [addComponentContent](../reference/apis-arkui/js-apis-arkui-frameNode.md#addcomponentcontent12)| 1196| [NodeContent](../reference/apis-arkui/js-apis-arkui-NodeContent.md) | [addFrameNode](../reference/apis-arkui/js-apis-arkui-NodeContent.md#addframenode12), [removeFrameNode](../reference/apis-arkui/js-apis-arkui-NodeContent.md#removeframenode12)| 1197| [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md) | [makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode) | 1198| [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md) | [appendChild](../reference/apis-arkui/js-apis-arkui-renderNode.md#appendchild), [insertChildAfter](../reference/apis-arkui/js-apis-arkui-renderNode.md#insertchildafter), [removeChild](../reference/apis-arkui/js-apis-arkui-renderNode.md#removechild), [clearChildren](../reference/apis-arkui/js-apis-arkui-renderNode.md#clearchildren)| 1199| [NodeAdaper](../reference/apis-arkui/js-apis-arkui-frameNode.md#nodeadapter12) | Lazy loading operations using [LazyForEach](../reference/apis-arkui/arkui-ts/ts-rendering-control-lazyforeach.md)| 1200 1201> **NOTE** 1202> 1203> When freeze inheritance is enabled, the BuilderNode's freeze policy stays synchronized with its nearest parent (custom component or BuilderNode). 1204> 1205> Frozen BuilderNodes ignore [update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update) calls. Requested node updates occur only after thawing. 1206 1207```ts 1208 1209import { BuilderNode, FrameNode, NodeController } from '@kit.ArkUI'; 1210 1211class Params { 1212 count: number = 0; 1213 1214 constructor(count: number) { 1215 this.count = count; 1216 } 1217} 1218 1219@Builder 1220function buildText(params: Params) { 1221 1222 Column() { 1223 TextBuilder({ message: params.count }) 1224 } 1225} 1226 1227class TextNodeController extends NodeController { 1228 private rootNode: FrameNode | null = null; 1229 private textNode: BuilderNode<[Params]> | null = null; 1230 private count: number = 0; 1231 1232 makeNode(context: UIContext): FrameNode | null { 1233 this.rootNode = new FrameNode(context); 1234 this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } }); 1235 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.count)); 1236 this.textNode.inheritFreezeOptions(true); // Configure the BuilderNode to inherit the freeze policy from its parent component. 1237 if (this.rootNode !== null) { 1238 this.rootNode.appendChild(this.textNode.getFrameNode()); // Mount the BuilderNode to the component tree. 1239 } 1240 return this.rootNode; 1241 } 1242 1243 update(): void { 1244 if (this.textNode !== null) { 1245 this.count += 1; 1246 this.textNode.update(new Params(this.count)); // Update the BuilderNode data, which triggers logs. 1247 } 1248 1249 } 1250} 1251 1252const textNodeController: TextNodeController = new TextNodeController(); 1253 1254@Entry 1255@Component 1256struct MyNavigationTestStack { 1257 @Provide('pageInfo') pageInfo: NavPathStack = new NavPathStack(); 1258 @State message: number = 0; 1259 @State logNumber: number = 0; 1260 1261 @Builder 1262 PageMap(name: string) { 1263 if (name === 'pageOne') { 1264 pageOneStack({ message: this.message, logNumber: this.logNumber }) 1265 } else if (name === 'pageTwo') { 1266 pageTwoStack({ message: this.message, logNumber: this.logNumber }) 1267 } 1268 } 1269 1270 build() { 1271 Column() { 1272 Button('update builderNode') // Clicking the button updates builderNode. 1273 .onClick(() => { 1274 textNodeController.update(); 1275 }) 1276 Navigation(this.pageInfo) { 1277 Column() { 1278 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 1279 .width('80%') 1280 .height(40) 1281 .margin(20) 1282 .onClick(() => { 1283 this.pageInfo.pushPath({ name: 'pageOne' }); // Push the navigation destination page specified by name to the navigation stack. 1284 }) 1285 } 1286 }.title('NavIndex') 1287 .navDestination(this.PageMap) 1288 .mode(NavigationMode.Stack) 1289 } 1290 } 1291} 1292 1293@Component 1294struct pageOneStack { 1295 @Consume('pageInfo') pageInfo: NavPathStack; 1296 @State index: number = 1; 1297 @Link message: number; 1298 @Link logNumber: number; 1299 1300 build() { 1301 NavDestination() { 1302 Column() { 1303 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 1304 Button('Next Page', { stateEffect: true, type: ButtonType.Capsule }) 1305 .width('80%') 1306 .height(40) 1307 .margin(20) 1308 .onClick(() => { 1309 this.pageInfo.pushPathByName('pageTwo', null); 1310 }) 1311 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 1312 .width('80%') 1313 .height(40) 1314 .margin(20) 1315 .onClick(() => { 1316 this.pageInfo.pop(); 1317 }) 1318 }.width('100%').height('100%') 1319 }.title('pageOne') 1320 .onBackPressed(() => { 1321 this.pageInfo.pop(); 1322 return true; 1323 }) 1324 } 1325} 1326 1327@Component 1328struct pageTwoStack { 1329 @Consume('pageInfo') pageInfo: NavPathStack; 1330 @State index: number = 2; 1331 @Link message: number; 1332 @Link logNumber: number; 1333 1334 build() { 1335 NavDestination() { 1336 Column() { 1337 NavigationContentMsgStack({ message: this.message, index: this.index, logNumber: this.logNumber }) 1338 Text('BuilderNode is frozen') 1339 .fontWeight(FontWeight.Bold) 1340 .margin({ top: 48, bottom: 48 }) 1341 Button('Back Page', { stateEffect: true, type: ButtonType.Capsule }) 1342 .width('80%') 1343 .height(40) 1344 .margin(20) 1345 .onClick(() => { 1346 this.pageInfo.pop(); 1347 }) 1348 }.width('100%').height('100%') 1349 }.title('pageTwo') 1350 .onBackPressed(() => { 1351 this.pageInfo.pop(); 1352 return true; 1353 }) 1354 } 1355} 1356 1357@Component({ freezeWhenInactive: true }) // Set the freeze policy to freeze when inactive. 1358struct NavigationContentMsgStack { 1359 @Link message: number; 1360 @Link index: number; 1361 @Link logNumber: number; 1362 1363 build() { 1364 Column() { 1365 if (this.index === 1) { 1366 NodeContainer(textNodeController) 1367 } 1368 } 1369 } 1370} 1371 1372@Component({ freezeWhenInactive: true }) // Set the freeze policy to freeze when inactive. 1373struct TextBuilder { 1374 @Prop @Watch("info") message: number = 0; 1375 1376 info() { 1377 console.info(`freeze-test TextBuilder message callback ${this.message}`); // Print logs based on the message content change to determine whether the freeze occurs. 1378 } 1379 1380 build() { 1381 Row() { 1382 Column() { 1383 Text(`Update count: ${this.message}`) 1384 .fontWeight(FontWeight.Bold) 1385 .margin({ top: 48, bottom: 48 }) 1386 } 1387 } 1388 } 1389} 1390``` 1391 1392 1393 1394## Configuring the BuilderNode for Cross-Boundary @Provide-@Consume Communication 1395 1396Since API version 20, the BuilderNode supports cross-boundary state sharing between [@Consume](./state-management/arkts-provide-and-consume.md) and [@Provide](./state-management/arkts-provide-and-consume.md) through the **BuildOptions** configuration. This feature enables seamless data flow from the host pages into BuilderNode's internal custom components. 1397 1398For implementation, see [Example](../reference/apis-arkui/js-apis-arkui-builderNode.md#example-7-configuring-the-buildernode-for-cross-boundary-provide-consume-communication). 1399 1400## Implementing Page Pre-Rendering with BuilderNode and Web Components 1401 1402Pre-rendering is particularly suitable for scenarios such as web page initialization and navigation transitions. By integrating with BuilderNode, **Web** components can be pre-rendered offline in advance. These components are not mounted to the page immediately, but rather dynamically attached and displayed through **NodeController** when needed. This approach significantly enhances page transition smoothness and improves user experience. 1403 1404> **NOTE** 1405> 1406> The **ohos.permission.INTERNET** permission is required for accessing online web pages. For details about how to apply for a permission, see [Declaring Permissions](../security/AccessToken/declare-permissions.md). 1407 14081. Create a host ability and a **Web** component. 1409 ```ts 1410 // Host ability 1411 // EntryAbility.ets 1412 import { createNWeb } from "../pages/common"; 1413 import { UIAbility } from '@kit.AbilityKit'; 1414 import { window } from '@kit.ArkUI'; 1415 1416 export default class EntryAbility extends UIAbility { 1417 onWindowStageCreate(windowStage: window.WindowStage): void { 1418 windowStage.loadContent('pages/Index', (err, data) => { 1419 // Create a dynamic Web component with UIContext. The component can be created at any time after loadContent() is called. 1420 createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext()); 1421 if (err.code) { 1422 return; 1423 } 1424 }); 1425 } 1426 } 1427 ``` 14282. Create a NodeContainer and the corresponding NodeController for background rendering. 1429 1430 ```ts 1431 // Create a NodeController instance. 1432 // common.ets 1433 import { UIContext } from '@kit.ArkUI'; 1434 import { webview } from '@kit.ArkWeb'; 1435 import { NodeController, BuilderNode, Size, FrameNode } from '@kit.ArkUI'; 1436 // @Builder content for dynamic component content 1437 // Data class for input parameters 1438 class Data{ 1439 url: string = 'https://www.example.com'; 1440 controller: WebviewController = new webview.WebviewController(); 1441 } 1442 // Use the Boolean variable shouldInactive to stop rendering after the web page is pre-rendered in the background. 1443 let shouldInactive: boolean = true; 1444 @Builder 1445 function WebBuilder(data:Data) { 1446 Column() { 1447 Web({ src: data.url, controller: data.controller }) 1448 .onPageBegin(() => { 1449 // Call onActive to enable rendering. 1450 data.controller.onActive(); 1451 }) 1452 .onFirstMeaningfulPaint(() =>{ 1453 if (!shouldInactive) { 1454 return; 1455 } 1456 // Triggered when the pre-rendering is complete to stop rendering. 1457 data.controller.onInactive(); 1458 shouldInactive = false; 1459 }) 1460 .width("100%") 1461 .height("100%") 1462 } 1463 } 1464 let wrap = wrapBuilder<Data[]>(WebBuilder); 1465 // The NodeController instance must be used with a NodeContainer for controlling and feeding back the behavior of the nodes in the NodeContainer. 1466 export class myNodeController extends NodeController { 1467 private rootnode: BuilderNode<Data[]> | null = null; 1468 // This function must be overridden, which is used to construct the number of nodes and return nodes to be mounted in NodeContainer. 1469 // Called when the corresponding NodeContainer is created or called by the rebuild method. 1470 makeNode(uiContext: UIContext): FrameNode | null { 1471 console.info(" uicontext is undifined : "+ (uiContext === undefined)); 1472 if (this.rootnode != null) { 1473 // Return the FrameNode object. 1474 return this.rootnode.getFrameNode(); 1475 } 1476 // Return null to detach the dynamic component from the bound node. 1477 return null; 1478 } 1479 // Called when the layout size changes. 1480 aboutToResize(size: Size) { 1481 console.info("aboutToResize width : " + size.width + " height : " + size.height ); 1482 } 1483 // Called when the NodeContainer bound to the controller is about to appear. 1484 aboutToAppear() { 1485 console.info("aboutToAppear"); 1486 // Keep rendering active when the page is brought to the foreground. 1487 shouldInactive = false; 1488 } 1489 // Called when the NodeContainer bound to the controller is about to disappear. 1490 aboutToDisappear() { 1491 console.info("aboutToDisappear"); 1492 } 1493 // This function is a custom function and can be used for initialization. 1494 // Initialize the BuilderNode through UIContext, and then initialize the content in @Builder through the build API in BuilderNode. 1495 initWeb(url:string, uiContext:UIContext, control:WebviewController) { 1496 if(this.rootnode != null){ 1497 return; 1498 } 1499 // Create a node, during which the UIContext should be passed. 1500 this.rootnode = new BuilderNode(uiContext); 1501 // Create a dynamic Web component. 1502 this.rootnode.build(wrap, { url:url, controller:control }); 1503 } 1504 } 1505 // Create a Map to store the required NodeController instance. 1506 let NodeMap:Map<string, myNodeController | undefined> = new Map(); 1507 // Create a Map to store the required WebViewController instance. 1508 let controllerMap:Map<string, WebviewController | undefined> = new Map(); 1509 // UIContext is required for initialization and needs to be obtained from the ability. 1510 export const createNWeb = (url: string, uiContext: UIContext) => { 1511 // Create a NodeController instance. 1512 let baseNode = new myNodeController(); 1513 let controller = new webview.WebviewController() ; 1514 // Initialize the custom Web component. 1515 baseNode.initWeb(url, uiContext, controller); 1516 controllerMap.set(url, controller); 1517 NodeMap.set(url, baseNode); 1518 } 1519 // Customize the API for obtaining the NodeController instance. 1520 export const getNWeb = (url : string) : myNodeController | undefined => { 1521 return NodeMap.get(url); 1522 } 1523 ``` 15243. Display the pre-rendered page through **NodeContainer**. 1525 1526 ```ts 1527 // Page component using NodeController 1528 // Index.ets 1529 import { createNWeb, getNWeb } from "./common"; 1530 1531 @Entry 1532 @Component 1533 struct Index { 1534 build() { 1535 Row() { 1536 Column() { 1537 // NodeContainer is used to bind to the NodeController. A rebuild call triggers makeNode. 1538 // The Page page is bound to the NodeController through the NodeContainer API to display the dynamic component. 1539 NodeContainer(getNWeb("https://www.example.com")) 1540 .height("90%") 1541 .width("100%") 1542 } 1543 .width('100%') 1544 } 1545 .height('100%') 1546 } 1547 } 1548 ``` 1549<!--no_check-->