• 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](../quick-start/arkts-builder.md#global-custom-builder-function), which is decorated by @Builder. Once your custom component tree is established, you can obtain its root [FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md) by calling [getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode). The root node can be directly returned by [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md) and mounted under a [NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md). **BuilderNode** facilitates embedding of embedding declarative components within **FrameNode** and [RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md) trees for mixed display. **BuilderNode** also offers a feature for exporting textures, which can be used for rendering within the same layer of the [XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md).
6
7The ArkTS built-in component tree constructed by **BuilderNode** can be used together with custom nodes, such as FrameNodes and RenderNodes, to achieve the mixed display effect. **BuilderNode** offers a suite of APIs designed to integrate built-in components within third-party frameworks. This is particularly beneficial for scenarios where these frameworks require interaction with custom nodes
8
9**BuilderNode** offers the capability to pre-create components, allowing you to dictate when built-in components are instantiated. This feature is useful for dynamically mounting and displaying components, especially for those that have a longer initialization period, such as [Web](../reference/apis-arkweb/ts-basic-components-web.md) and [XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md).
10
11![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(../quick-start/arkts-builder.md#global-custom-builder-function) decorated by @Builder and wrapped by [wrapBuilder](../quick-start/arkts-wrapBuilder.md).
24>
25> - A newly created BuilderNode can only obtain a **FrameNode** object pointing to the root node through [getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode) after [build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build); otherwise, it returns **null**.
26>
27> - If the root node of the passed Builder is a syntactic node (such as **if/else** and **ForEach**), an additional FrameNode must be generated, which will be displayed as "BuilderProxyNode" in the node tree.
28>
29> - If BuilderNode mounts a node onto another FrameNode through **getFrameNode**, or mounts it as a child node onto a **NodeContainer**, the node uses the layout constraints of the parent component for layout.
30>
31> - If a BuilderNode's FrameNode mounts its node onto a RenderNode through [getRenderNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#getrendernode), its size defaults to **0** since its FrameNode is not yet part of the tree. To display it properly, you must explicitly specify the layout constraint size through [selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions) in the constructor.
32>
33> - Pre-creation with the BuilderNode does not reduce the creation time of components. For the **Web** component, resources must be loaded in the kernel during creation, and pre-creation cannot reduce this time. However, it enables the kernel to preload resources, which can reduce the loading time when the component is used.
34
35## Creating a BuilderNode Object
36
37When creating a **BuilderNode** object, which is a template class, you must specify a type that matches the type of the [WrappedBuilder](../quick-start/arkts-wrapBuilder.md) used in the **build** method later on. Mismatches can cause compilation warnings and failures.
38
39## Creating a Built-in Component Tree
40
41Use the **build** API of **BuilderNode** to create a built-in component tree. The tree is constructed based on the **WrappedBuilder** object passed in, and the root node of the component tree is retained.
42
43> **NOTE**
44>
45> Stateless UI methods using the global @Builder can have at most one root node.
46>
47> The @Builder within the **build** method accepts only one input parameter.
48>
49> In scenarios where @Builder is nested within another @Builder in the **build** method, ensure that the parameters of the nested @Builder match the input parameters provided to the **build** method.
50>
51> For scenarios where @Builder is nested within another @Builder, if the parameter types do not match, you must include the [BuilderOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#buildoptions12) field as a parameter for the [build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build12) method.
52>
53> To operate objects in a BuilderNode, ensure that the reference to the BuilderNode is not garbage collected. Once a BuilderNode object is collected by the virtual machine, its FrameNode and RenderNode objects will also be dereferenced from the backend nodes. This means that any FrameNode objects obtained from a BuilderNode will no longer correspond to any actual node if the BuilderNode is garbage collected.
54
55Create offline nodes and built-in component trees, and use them in conjunction with FrameNodes.
56
57The root node of the BuilderNode is directly used as the return value of [makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode) of [NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md).
58
59```ts
60import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';
61
62class Params {
63  text: string = "";
64
65  constructor(text: string) {
66    this.text = text;
67  }
68}
69
70@Builder
71function buildText(params: Params) {
72  Column() {
73    Text(params.text)
74      .fontSize(50)
75      .fontWeight(FontWeight.Bold)
76      .margin({ bottom: 36 })
77  }
78}
79
80class TextNodeController extends NodeController {
81  private textNode: BuilderNode<[Params]> | null = null;
82  private message: string = "DEFAULT";
83
84  constructor(message: string) {
85    super();
86    this.message = message;
87  }
88
89  makeNode(context: UIContext): FrameNode | null {
90    this.textNode = new BuilderNode(context);
91    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
92    return this.textNode.getFrameNode();
93  }
94}
95
96@Entry
97@Component
98struct Index {
99  @State message: string = "hello";
100
101  build() {
102    Row() {
103      Column() {
104        NodeContainer(new TextNodeController(this.message))
105          .width('100%')
106          .height(100)
107          .backgroundColor('#FFF0F0F0')
108      }
109      .width('100%')
110      .height('100%')
111    }
112    .height('100%')
113  }
114}
115```
116
117When combining a BuilderNode with a RenderNode, note the following:
118
119If you mount the RenderNode from the BuilderNode under another RenderNode, you must explicitly specify [selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions) as the layout constraint for the BuilderNode. This approach to mounting nodes is not recommended.
120
121```ts
122import { NodeController, BuilderNode, FrameNode, UIContext, RenderNode } from "@kit.ArkUI";
123
124class Params {
125  text: string = "";
126
127  constructor(text: string) {
128    this.text = text;
129  }
130}
131
132@Builder
133function buildText(params: Params) {
134  Column() {
135    Text(params.text)
136      .fontSize(50)
137      .fontWeight(FontWeight.Bold)
138      .margin({ bottom: 36 })
139  }
140}
141
142class TextNodeController extends NodeController {
143  private rootNode: FrameNode | null = null;
144  private textNode: BuilderNode<[Params]> | null = null;
145  private message: string = "DEFAULT";
146
147  constructor(message: string) {
148    super();
149    this.message = message;
150  }
151
152  makeNode(context: UIContext): FrameNode | null {
153    this.rootNode = new FrameNode(context);
154    let renderNode = new RenderNode();
155    renderNode.clipToFrame = false;
156    this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } });
157    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
158    const textRenderNode = this.textNode?.getFrameNode()?.getRenderNode();
159
160    const rootRenderNode = this.rootNode.getRenderNode();
161    if (rootRenderNode !== null) {
162      rootRenderNode.appendChild(renderNode);
163      renderNode.appendChild(textRenderNode);
164    }
165
166    return this.rootNode;
167  }
168}
169
170@Entry
171@Component
172struct Index {
173  @State message: string = "hello";
174
175  build() {
176    Row() {
177      Column() {
178        NodeContainer(new TextNodeController(this.message))
179          .width('100%')
180          .height(100)
181          .backgroundColor('#FFF0F0F0')
182      }
183      .width('100%')
184      .height('100%')
185    }
186    .height('100%')
187  }
188}
189```
190
191## Updating the Built-in Component Tree
192
193Create a built-in component tree using the **build** API of a **BuilderNode** object. The tree is constructed based on the **WrappedBuilder** object passed in, and the root node of the component tree is retained.
194
195Custom component updates follow the update mechanisms of [state management](../quick-start/arkts-state-management-overview.md). For custom components used directly in a **WrappedBuilder** object, their parent component is the **BuilderNode** object. Therefore, to update child components defined in the **WrappedBuilder** objects, you need to define the relevant state variables with the [\@Prop](../quick-start/arkts-prop.md) or [\@ObjectLink](../quick-start/arkts-observed-and-objectlink.md) decorator, in accordance with the specifications of state management and the needs of your application development.
196
197
198To update nodes within a BuilderNode:<br>Use the **update** API to update individual nodes within the BuilderNode.
199
200Use the [updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12) API to trigger a full update of all nodes within the BuilderNode.
201
202
203
204```ts
205import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
206
207class Params {
208  text: string = "";
209  constructor(text: string) {
210    this.text = text;
211  }
212}
213
214// Custom component
215@Component
216struct TextBuilder {
217  // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute.
218  @Prop message: string = "TextBuilder";
219
220  build() {
221    Row() {
222      Column() {
223        Text(this.message)
224          .fontSize(50)
225          .fontWeight(FontWeight.Bold)
226          .margin({ bottom: 36 })
227          .backgroundColor(Color.Gray)
228      }
229    }
230  }
231}
232
233@Builder
234function buildText(params: Params) {
235  Column() {
236    Text(params.text)
237      .fontSize(50)
238      .fontWeight(FontWeight.Bold)
239      .margin({ bottom: 36 })
240    TextBuilder({ message: params.text }) // Custom component
241  }
242}
243
244class TextNodeController extends NodeController {
245  private textNode: BuilderNode<[Params]> | null = null;
246  private message: string = "";
247
248  constructor(message: string) {
249    super()
250    this.message = message
251  }
252
253  makeNode(context: UIContext): FrameNode | null {
254    this.textNode = new BuilderNode(context);
255    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
256    return this.textNode.getFrameNode();
257  }
258
259  update(message: string) {
260    if (this.textNode !== null) {
261      // Call update to perform an update.
262      this.textNode.update(new Params(message));
263    }
264  }
265}
266
267@Entry
268@Component
269struct Index {
270  @State message: string = "hello";
271  private textNodeController: TextNodeController = new TextNodeController(this.message);
272  private count = 0;
273
274  build() {
275    Row() {
276      Column() {
277        NodeContainer(this.textNodeController)
278          .width('100%')
279          .height(200)
280          .backgroundColor('#FFF0F0F0')
281        Button('Update')
282          .onClick(() => {
283            this.count += 1;
284            const message = "Update " + this.count.toString();
285            this.textNodeController.update(message);
286          })
287      }
288      .width('100%')
289      .height('100%')
290    }
291    .height('100%')
292  }
293}
294```
295
296## Canceling the Reference to the Entity Node
297
298A **BuilderNode** object is mapped to a backend entity node, and its memory release is usually contingent on the disposal of the frontend object. To directly release the backend node object, you can call the [dispose](../reference/apis-arkui/js-apis-arkui-builderNode.md#dispose12) API to break the reference to the entity node. Once this is done, the frontend **BuilderNode** object will no longer affect the lifecycle of the entity node.
299
300> **NOTE**
301>
302> Calling **dispose** on a **BuilderNode** object breaks its reference to the backend entity node, and also simultaneously severs the references of its contained FrameNode and RenderNode to their respective entity nodes.
303>
304> If the frontend object BuilderNode cannot be released, memory leaks may occur. To avoid this, be sure to call **dispose** on the BuilderNode when you no longer need it. This reduces the complexity of reference relationships and lowers the risk of memory leaks.
305
306## Injecting a Touch Event
307
308Use the [postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent) API in the BuilderNode to inject a [touch event](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md) into the bound component for event simulation and forwarding.
309
310
311
312The following example forwards a touch event from one **Column** component to another in the BuilderNode, so that when the lower **Column** component is touched, the upper **Column** component also receives the same touch event. The API returns **true** if the button's event is successfully recognized.
313
314```ts
315import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';
316
317class Params {
318  text: string = "this is a text";
319}
320
321@Builder
322function ButtonBuilder(params: Params) {
323  Column() {
324    Button(`button ` + params.text)
325      .borderWidth(2)
326      .backgroundColor(Color.Orange)
327      .width("100%")
328      .height("100%")
329      .gesture(
330        TapGesture()
331          .onAction((event: GestureEvent) => {
332            console.log("TapGesture");
333          })
334      )
335  }
336  .width(500)
337  .height(300)
338  .backgroundColor(Color.Gray)
339}
340
341class MyNodeController extends NodeController {
342  private rootNode: BuilderNode<[Params]> | null = null;
343  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder);
344
345  makeNode(uiContext: UIContext): FrameNode | null {
346    this.rootNode = new BuilderNode(uiContext);
347    this.rootNode.build(this.wrapBuilder, { text: "this is a string" })
348    return this.rootNode.getFrameNode();
349  }
350
351  postTouchEvent(touchEvent: TouchEvent): void {
352    if (this.rootNode == null) {
353      return;
354    }
355    let result = this.rootNode.postTouchEvent(touchEvent);
356    console.log("result " + result);
357  }
358}
359
360@Entry
361@Component
362struct MyComponent {
363  private nodeController: MyNodeController = new MyNodeController();
364
365  build() {
366    Column() {
367      NodeContainer(this.nodeController)
368        .height(300)
369        .width(500)
370      Column()
371        .width(500)
372        .height(300)
373        .backgroundColor(Color.Pink)
374        .onTouch((event) => {
375          if (event != undefined) {
376            this.nodeController.postTouchEvent(event);
377          }
378        })
379    }
380  }
381}
382```
383
384## Reusing a BuilderNode
385
386To implement component reuse within a BuilderNode, you need to call the [reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12) and [recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12) APIs. These APIs pass reuse and recycle events to custom components inside the BuilderNode.
387
388For example, in the following demo, the custom component **ReusableChildComponent** can pass reuse and recycle events to its nested custom component **ReusableChildComponent3**. However, these events cannot automatically reach another custom component, **ReusableChildComponent2**, if it is separated by a BuilderNode. To enable reuse for **ReusableChildComponent2**, you must explicitly call the **reuse** and **recycle** APIs on the BuilderNode to forward these events to **ReusableChildComponent2**.
389![en-us_image_reuse-recycle](figures/reuse-recycle.png)
390
391
392```ts
393import { FrameNode, NodeController, BuilderNode, UIContext } from "@kit.ArkUI";
394
395const TEST_TAG: string = "Reuse+Recycle";
396
397class MyDataSource {
398  private dataArray: string[] = [];
399  private listener: DataChangeListener | null = null
400
401  public totalCount(): number {
402    return this.dataArray.length;
403  }
404
405  public getData(index: number) {
406    return this.dataArray[index];
407  }
408
409  public pushData(data: string) {
410    this.dataArray.push(data);
411  }
412
413  public reloadListener(): void {
414    this.listener?.onDataReloaded();
415  }
416
417  public registerDataChangeListener(listener: DataChangeListener): void {
418    this.listener = listener;
419  }
420
421  public unregisterDataChangeListener(): void {
422    this.listener = null;
423  }
424}
425
426class Params {
427  item: string = '';
428
429  constructor(item: string) {
430    this.item = item;
431  }
432}
433
434@Builder
435function buildNode(param: Params = new Params("hello")) {
436  Row() {
437    Text(`C${param.item} -- `)
438    ReusableChildComponent2({ item: param.item }) // This custom component cannot be correctly reused in the BuilderNode.
439  }
440}
441
442class MyNodeController extends NodeController {
443  public builderNode: BuilderNode<[Params]> | null = null;
444  public item: string = "";
445
446  makeNode(uiContext: UIContext): FrameNode | null {
447    if (this.builderNode == null) {
448      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
449      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
450    }
451    return this.builderNode.getFrameNode();
452  }
453}
454
455// The custom component that is reused and recycled will have its state variables updated, and the state variables of the nested custom component ReusableChildComponent3 will also be updated. However, the BuilderNode will block this propagation process.
456@Reusable
457@Component
458struct ReusableChildComponent {
459  @Prop item: string = '';
460  @Prop switch: string = '';
461  private controller: MyNodeController = new MyNodeController();
462
463  aboutToAppear() {
464    this.controller.item = this.item;
465  }
466
467  aboutToRecycle(): void {
468    console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`);
469
470    // When the switch is open, pass the recycle event to the nested custom component, such as ReusableChildComponent2, through the BuilderNode's recycle API to complete recycling.
471    if (this.switch === 'open') {
472      this.controller?.builderNode?.recycle();
473    }
474  }
475
476  aboutToReuse(params: object): void {
477    console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`);
478
479    // When the switch is open, pass the reuse event to the nested custom component, such as ReusableChildComponent2, through the BuilderNode's reuse API to complete reuse.
480    if (this.switch === 'open') {
481      this.controller?.builderNode?.reuse(params);
482    }
483  }
484
485  build() {
486    Row() {
487      Text(`A${this.item}--`)
488      ReusableChildComponent3({ item: this.item })
489      NodeContainer(this.controller);
490    }
491  }
492}
493
494@Component
495struct ReusableChildComponent2 {
496  @Prop item: string = "false";
497
498  aboutToReuse(params: Record<string, object>) {
499    console.log(`${TEST_TAG} ReusableChildComponent2 aboutToReuse ${JSON.stringify(params)}`);
500  }
501
502  aboutToRecycle(): void {
503    console.log(`${TEST_TAG} ReusableChildComponent2 aboutToRecycle ${this.item}`);
504  }
505
506  build() {
507    Row() {
508      Text(`D${this.item}`)
509        .fontSize(20)
510        .backgroundColor(Color.Yellow)
511        .margin({ left: 10 })
512    }.margin({ left: 10, right: 10 })
513  }
514}
515
516@Component
517struct ReusableChildComponent3 {
518  @Prop item: string = "false";
519
520  aboutToReuse(params: Record<string, object>) {
521    console.log(`${TEST_TAG} ReusableChildComponent3 aboutToReuse ${JSON.stringify(params)}`);
522  }
523
524  aboutToRecycle(): void {
525    console.log(`${TEST_TAG} ReusableChildComponent3 aboutToRecycle ${this.item}`);
526  }
527
528  build() {
529    Row() {
530      Text(`B${this.item}`)
531        .fontSize(20)
532        .backgroundColor(Color.Yellow)
533        .margin({ left: 10 })
534    }.margin({ left: 10, right: 10 })
535  }
536}
537
538
539@Entry
540@Component
541struct Index {
542  @State data: MyDataSource = new MyDataSource();
543
544  aboutToAppear() {
545    for (let i = 0; i < 100; i++) {
546      this.data.pushData(i.toString());
547    }
548  }
549
550  build() {
551    Column() {
552      List({ space: 3 }) {
553        LazyForEach(this.data, (item: string) => {
554          ListItem() {
555            ReusableChildComponent({
556              item: item,
557              switch: 'open' // Changing open to close can be used to observe the behavior of custom components inside the BuilderNode when reuse and recycle events are not passed through the BuilderNode's reuse and recycle APIs.
558            })
559          }
560        }, (item: string) => item)
561      }
562      .width('100%')
563      .height('100%')
564    }
565  }
566}
567```
568
569
570## Updating Nodes Based on System Environment Changes
571
572Use the [updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12) API to listen for [system environment changes](../reference/apis-ability-kit/js-apis-app-ability-configuration.md). This will trigger a full update of all nodes within the BuilderNode.
573
574> **NOTE**
575>
576> The **updateConfiguration** API is designed to inform objects of the need to update, with the updates reflecting changes in the application's current system environment.
577
578```ts
579import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
580import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit';
581
582class Params {
583  text: string = ""
584
585  constructor(text: string) {
586    this.text = text;
587  }
588}
589
590// Custom component
591@Component
592struct TextBuilder {
593  // The @Prop decorated attribute is the attribute to be updated in the custom component. It is a basic attribute.
594  @Prop message: string = "TextBuilder";
595
596  build() {
597    Row() {
598      Column() {
599        Text(this.message)
600          .fontSize(50)
601          .fontWeight(FontWeight.Bold)
602          .margin({ bottom: 36 })
603          .fontColor($r(`app.color.text_color`))
604          .backgroundColor($r(`app.color.start_window_background`))
605      }
606    }
607  }
608}
609
610@Builder
611function buildText(params: Params) {
612  Column() {
613    Text(params.text)
614      .fontSize(50)
615      .fontWeight(FontWeight.Bold)
616      .margin({ bottom: 36 })
617      .fontColor($r(`app.color.text_color`))
618    TextBuilder({ message: params.text }) // Custom component
619  }.backgroundColor($r(`app.color.start_window_background`))
620}
621
622class TextNodeController extends NodeController {
623  private textNode: BuilderNode<[Params]> | null = null;
624  private message: string = "";
625
626  constructor(message: string) {
627    super()
628    this.message = message;
629  }
630
631  makeNode(context: UIContext): FrameNode | null {
632    return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null;
633  }
634
635  createNode(context: UIContext) {
636    this.textNode = new BuilderNode(context);
637    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
638    builderNodeMap.push(this.textNode);
639  }
640
641  deleteNode() {
642    let node = builderNodeMap.pop();
643    node?.dispose();
644  }
645
646  update(message: string) {
647    if (this.textNode !== null) {
648      // Call update to perform an update.
649      this.textNode.update(new Params(message));
650    }
651  }
652}
653
654// Record the created custom node object.
655const builderNodeMap: Array<BuilderNode<[Params]>> = new Array();
656
657function updateColorMode() {
658  builderNodeMap.forEach((value, index) => {
659    // Notify BuilderNode of the environment changes.
660    value.updateConfiguration();
661  })
662}
663
664@Entry
665@Component
666struct Index {
667  @State message: string = "hello"
668  private textNodeController: TextNodeController = new TextNodeController(this.message);
669  private count = 0;
670
671  aboutToAppear(): void {
672    let environmentCallback: EnvironmentCallback = {
673      onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => {
674        console.log('onMemoryLevel');
675      },
676      onConfigurationUpdated: (config: Configuration): void => {
677        console.log('onConfigurationUpdated ' + JSON.stringify(config));
678        updateColorMode();
679      }
680    }
681    // Register a callback.
682    this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback);
683    // Create a custom node and add it to the map.
684    this.textNodeController.createNode(this.getUIContext());
685  }
686
687  aboutToDisappear(): void {
688    // Remove the reference to the custom node from the map and release the node.
689    this.textNodeController.deleteNode();
690  }
691
692  build() {
693    Row() {
694      Column() {
695        NodeContainer(this.textNodeController)
696          .width('100%')
697          .height(200)
698          .backgroundColor('#FFF0F0F0')
699        Button('Update')
700          .onClick(() => {
701            this.count += 1;
702            const message = "Update " + this.count.toString();
703            this.textNodeController.update(message);
704          })
705      }
706      .width('100%')
707      .height('100%')
708    }
709    .height('100%')
710  }
711}
712```
713
714## Cross-Page Reuse Considerations
715
716With use of [routing](../reference/apis-arkui/js-apis-router.md) APIs such as [router.replaceUrl](../reference/apis-arkui/js-apis-router.md#routerreplaceurl9), [router.back](../reference/apis-arkui/js-apis-router.md#routerback), [router.clear](../reference/apis-arkui/js-apis-router.md#routerclear), and [router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10) to navigate between pages, issues may arise when you reuse a cached BuilderNode from a page that is about to be destroyed. Specifically, the reused BuilderNode might not update its data correctly, or newly created nodes might not display as expected. For example, when you use [router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10), consider the following scenario: When the **router replace** button is clicked, the page switches to PageTwo, and the flag **isShowText** is set to **false**.
717
718```ts
719// ets/pages/Index.ets
720import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
721import "ets/pages/PageTwo"
722
723@Builder
724function buildText() {
725  // Use syntax nodes to generate a BuilderProxyNode within @Builder.
726  if (true) {
727    MyComponent()
728  }
729}
730
731@Component
732struct MyComponent {
733  @StorageLink("isShowText") isShowText: boolean = true;
734
735  build() {
736    if (this.isShowText) {
737      Column() {
738        Text("BuilderNode Reuse")
739          .fontSize(36)
740          .fontWeight(FontWeight.Bold)
741          .padding(16)
742      }
743    }
744  }
745}
746
747class TextNodeController extends NodeController {
748  private rootNode: FrameNode | null = null;
749  private textNode: BuilderNode<[]> | null = null;
750
751  makeNode(context: UIContext): FrameNode | null {
752    this.rootNode = new FrameNode(context);
753
754    if (AppStorage.has("textNode")) {
755      // Reuse the BuilderNode from AppStorage.
756      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
757      const parent = this.textNode.getFrameNode()?.getParent();
758      if (parent) {
759        parent.removeChild(this.textNode.getFrameNode());
760      }
761    } else {
762      this.textNode = new BuilderNode(context);
763      this.textNode.build(wrapBuilder<[]>(buildText));
764      // Save the created BuilderNode to AppStorage.
765      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
766    }
767    this.rootNode.appendChild(this.textNode.getFrameNode());
768
769    return this.rootNode;
770  }
771}
772
773@Entry({ routeName: "myIndex" })
774@Component
775struct Index {
776  aboutToAppear(): void {
777    AppStorage.setOrCreate<boolean>("isShowText", true);
778  }
779
780  build() {
781    Row() {
782      Column() {
783        NodeContainer(new TextNodeController())
784          .width('100%')
785          .backgroundColor('#FFF0F0F0')
786        Button('Router pageTwo')
787          .onClick(() => {
788            // Change the state variable in AppStorage to trigger re-creation of the Text node.
789            AppStorage.setOrCreate<boolean>("isShowText", false);
790
791            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
792          })
793          .margin({ top: 16 })
794      }
795      .width('100%')
796      .height('100%')
797      .padding(16)
798    }
799    .height('100%')
800  }
801}
802```
803
804The implementation of **PageTwo** is as follows:
805
806```ts
807// ets/pages/PageTwo.ets
808// This page contains a button to navigate back to the home page, where the original text disappears.
809import "ets/pages/Index"
810
811@Entry({ routeName: "pageTwo" })
812@Component
813struct PageTwo {
814  build() {
815    Column() {
816      Button('Router replace to index')
817        .onClick(() => {
818          this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" });
819        })
820    }
821    .height('100%')
822    .width('100%')
823    .alignItems(HorizontalAlign.Center)
824    .padding(16)
825  }
826}
827```
828
829![BuilderNode Reuse Example](./figures/builder_node_reuse.gif)
830
831In versions earlier than API version 16, you need to manually remove the BuilderNode from the cache, AppStorage in this example, when the page is destroyed.
832
833Since API version 16, the BuilderNode automatically refreshes its content when reused in a new page. This means you no longer need to remove the BuilderNode from the cache when the page is destroyed.
834
835```ts
836// ets/pages/Index.ets
837import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
838import "ets/pages/PageTwo"
839
840@Builder
841function buildText() {
842  // Use syntax nodes to generate a BuilderProxyNode within @Builder.
843  if (true) {
844    MyComponent()
845  }
846}
847
848@Component
849struct MyComponent {
850  @StorageLink("isShowText") isShowText: boolean = true;
851
852  build() {
853    if (this.isShowText) {
854      Column() {
855        Text("BuilderNode Reuse")
856          .fontSize(36)
857          .fontWeight(FontWeight.Bold)
858          .padding(16)
859      }
860    }
861  }
862}
863
864class TextNodeController extends NodeController {
865  private rootNode: FrameNode | null = null;
866  private textNode: BuilderNode<[]> | null = null;
867
868  makeNode(context: UIContext): FrameNode | null {
869    this.rootNode = new FrameNode(context);
870
871    if (AppStorage.has("textNode")) {
872      // Reuse the BuilderNode from AppStorage.
873      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
874      const parent = this.textNode.getFrameNode()?.getParent();
875      if (parent) {
876        parent.removeChild(this.textNode.getFrameNode());
877      }
878    } else {
879      this.textNode = new BuilderNode(context);
880      this.textNode.build(wrapBuilder<[]>(buildText));
881      // Save the created BuilderNode to AppStorage.
882      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
883    }
884    this.rootNode.appendChild(this.textNode.getFrameNode());
885
886    return this.rootNode;
887  }
888}
889
890@Entry({ routeName: "myIndex" })
891@Component
892struct Index {
893  aboutToAppear(): void {
894    AppStorage.setOrCreate<boolean>("isShowText", true);
895  }
896
897  build() {
898    Row() {
899      Column() {
900        NodeContainer(new TextNodeController())
901          .width('100%')
902          .backgroundColor('#FFF0F0F0')
903        Button('Router pageTwo')
904          .onClick(() => {
905            // Change the state variable in AppStorage to trigger re-creation of the Text node.
906            AppStorage.setOrCreate<boolean>("isShowText", false);
907            // Remove the BuilderNode from AppStorage.
908            AppStorage.delete("textNode");
909
910            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
911          })
912          .margin({ top: 16 })
913      }
914      .width('100%')
915      .height('100%')
916      .padding(16)
917    }
918    .height('100%')
919  }
920}
921```
922
923
924## Using the LocalStorage in the BuilderNode
925
926Since API version 12, custom components can receive [LocalStorage](../quick-start/arkts-localstorage.md) instances. You can use LocalStorage related decorators such as [@LocalStorageProp](../quick-start/arkts-localstorage.md#localstorageprop) and [@LocalStorageLink](../quick-start/arkts-localstorage.md#localstoragelink) by [passing LocalStorage instances](../quick-start/arkts-localstorage.md#example-of-providing-a-custom-component-with-access-to-a-localstorage-instance).
927
928```ts
929import { BuilderNode, NodeController, UIContext } from '@kit.ArkUI';
930
931let localStorage1: LocalStorage = new LocalStorage();
932localStorage1.setOrCreate('PropA', 'PropA');
933
934let localStorage2: LocalStorage = new LocalStorage();
935localStorage2.setOrCreate('PropB', 'PropB');
936
937@Entry(localStorage1)
938@Component
939struct Index {
940  // PropA is in two-way synchronization with PropA in localStorage1.
941  @LocalStorageLink('PropA') PropA: string = 'Hello World';
942  @State count: number = 0;
943  private controller: NodeController = new MyNodeController(this.count, localStorage2);
944
945  build() {
946    Row() {
947      Column() {
948        Text(this.PropA)
949          .fontSize(50)
950          .fontWeight(FontWeight.Bold)
951        // Use the LocalStorage instance localStorage2.
952        Child({ count: this.count }, localStorage2)
953        NodeContainer(this.controller)
954      }
955      .width('100%')
956    }
957    .height('100%')
958  }
959}
960
961interface Params {
962  count: number;
963  localStorage: LocalStorage;
964}
965
966@Builder
967function CreateChild(params: Params) {
968  // Pass localStorage during construction.
969  Child({ count: params.count }, params.localStorage)
970}
971
972class MyNodeController extends NodeController {
973  private count?: number;
974  private localStorage ?: LocalStorage;
975
976  constructor(count: number, localStorage: LocalStorage) {
977    super();
978    this.count = count;
979    this.localStorage = localStorage;
980  }
981
982  makeNode(uiContext: UIContext): FrameNode | null {
983    let builderNode = new BuilderNode<[Params]>(uiContext);
984    // Pass localStorage during construction.
985    builderNode.build(wrapBuilder(CreateChild), { count: this.count, localStorage: this.localStorage });
986    return builderNode.getFrameNode();
987  }
988}
989
990@Component
991struct Child {
992  @Prop count: number;
993  // 'Hello World' is in two-way synchronization with PropB in localStorage2. If there is no PropB in localStorage2, the default value 'Hello World' is used.
994  @LocalStorageLink('PropB') PropB: string = 'Hello World';
995
996  build() {
997    Text(this.PropB)
998      .fontSize(50)
999      .fontWeight(FontWeight.Bold)
1000  }
1001}
1002```
1003