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