• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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![builder-node](figures/builder-node.png)
12
13## Basic Concepts
14
15- [Built-in component](arkts-ui-development-overview.md): component provided directly by ArkUI. Components are essential elements of the UI, working together to shape the UI.
16
17- Entity node: native node created by the backend.
18
19A BuilderNode can be used only as a leaf node. If an update is required, you are advised to use the [update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update) API provided by the BuilderNode, rather than making modifications directly to the RenderNode obtained from it.
20
21> **NOTE**
22>
23> - The BuilderNode only supports a single [global custom build function](../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![en-us_image_reuse-recycle](figures/reuse-recycle.png)
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![BuilderNode-Reusable](figures/BuilderNode-Reusable.png)
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![BuilderNode Reuse Example](./figures/builder_node_reuse.gif)
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-->