1# 自定义声明式节点 (BuilderNode) 2 3## 概述 4 5自定义声明式节点 ([BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md))提供能够挂载系统组件的能力,支持采用无状态的UI方式,通过[全局自定义构建函数](../quick-start/arkts-builder.md#全局自定义构建函数)@Builder定制组件树。组件树的根[FrameNode](../reference/apis-arkui/js-apis-arkui-frameNode.md)节点可通过[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取,该节点既可直接由[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)返回并挂载于[NodeContainer](../reference/apis-arkui/arkui-ts/ts-basic-components-nodecontainer.md)节点下,亦可在FrameNode树与[RenderNode](../reference/apis-arkui/js-apis-arkui-renderNode.md)树中嵌入声明式组件,实现混合显示。同时,BuilderNode具备纹理导出功能,导出的纹理可在[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)中实现同层渲染。 6 7由BuilderNode构建的ArkTS原生控件树,支持与自定义节点(如FrameNode、RenderNode)关联使用,确保了系统组件与自定义节点的混合显示效果。对于需与自定义节点对接的第三方框架,BuilderNode提供了嵌入系统组件的方法。 8 9此外,BuilderNode还提供了组件预创建的能力,能够自定义系统组件的创建开始的时间,在后续业务中实现动态挂载与显示。此功能尤其适用于初始化耗时较长的声明式组件,如[Web](../reference/apis-arkweb/ts-basic-components-web.md)、[XComponent](../reference/apis-arkui/arkui-ts/ts-basic-components-xcomponent.md)等,通过预创建,可以有效减少初始化时间,优化组件加载效率。 10 11 12 13## 基本概念 14 15- 系统组件:组件是UI的必要元素,形成了在界面中的样子,由ArkUI直接提供的称为[系统组件](arkts-ui-development-overview.md)。 16 17- 实体节点:由后端创建的Native节点。 18 19BuilderNode仅可作为叶子节点进行使用。如有更新需要,建议通过BuilderNode中的[update](../reference/apis-arkui/js-apis-arkui-builderNode.md#update)方式触发更新,不建议通过BuilderNode中获取的RenderNode对节点进行修改操作。 20 21> **说明:** 22> 23> - BuilderNode只支持一个由[wrapBuilder](../quick-start/arkts-wrapBuilder.md)包装的[全局自定义构建函数](../quick-start/arkts-builder.md#全局自定义构建函数)@Builder。 24> 25> - 一个新建的BuildNode在[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build)之后才能通过[getFrameNode](../reference/apis-arkui/js-apis-arkui-builderNode.md#getframenode)获取到一个指向根节点的FrameNode对象,否则返回null。 26> 27> - 如果传入的Builder的根节点为语法节点(if/else/foreach/...),需要额外生成一个FrameNode,在节点树中的显示为“BuilderProxyNode”。 28> 29> - 如果BuilderNode通过getFrameNode将节点挂载在另一个FrameNode上,或者将其作为子节点挂载在NodeContainer节点上。则节点中使用父组件的布局约束进行布局。 30> 31> - 如果BuilderNode的FrameNode通过[getRenderNode](../reference/apis-arkui/js-apis-arkui-frameNode.md#getrendernode)形式将自己的节点挂载在RenderNode节点上,由于其FrameNode未上树,其大小默认为0,需要通过构造函数中的[selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions)显式指定布局约束大小,才能正常显示。 32> 33> - BuilderNode的预加载并不会减少组件的创建时间。Web组件创建的时候需要在内核中加载资源,预创建不能减少Web组件的创建的时间,但是可以让内核进行预加载,减少正式使用时候内核的加载耗时。 34 35## 创建BuilderNode对象 36 37BuilderNode对象为一个模板类,需要在创建的时候指定类型。该类型需要与后续build方法中传入的[WrappedBuilder](../quick-start/arkts-wrapBuilder.md#wrapbuilder封装全局builder)的类型保持一致,否则会存在编译告警导致编译失败。 38 39## 创建原生组件树 40 41通过BuilderNode的build可以实现原生组件树的创建。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。 42 43> **说明:** 44> 45> 无状态的UI方法全局@Builder最多拥有一个根节点。 46> 47> build方法中对应的@Builder支持一个参数作为入参。 48> 49> build中对于@Builder嵌套@Builder进行使用的场景,需要保证嵌套的参数与build的中提供的入参一致。 50> 51> 对于@Builder嵌套@Builder进行使用的场景,如果入参类型不一致,则要求增加[BuilderOptions](../reference/apis-arkui/js-apis-arkui-builderNode.md#buildoptions12)字段作为[build](../reference/apis-arkui/js-apis-arkui-builderNode.md#build12)的入参。 52> 53> 需要操作BuilderNode中的对象时,需要保证其引用不被回收。当BuilderNode对象被虚拟机回收之后,它的FrameNode、RenderNode对象也会与后端节点解引用。即从BuilderNode中获取的FrameNode对象不对应任何一个节点。 54 55创建离线节点以及原生组件树,结合FrameNode进行使用。 56 57BuilderNode的根节点直接作为[NodeController](../reference/apis-arkui/js-apis-arkui-nodeController.md)的[makeNode](../reference/apis-arkui/js-apis-arkui-nodeController.md#makenode)返回值。 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 117将BuilderNode与RenderNode进行结合使用。 118 119BuilderNode的RenderNode挂载其它RenderNode下时,需要明确定义[selfIdeaSize](../reference/apis-arkui/js-apis-arkui-builderNode.md#renderoptions)的大小作为BuilderNode的布局约束。不推荐通过该方式挂载节点。 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## 更新原生组件树 192 193通过BuilderNode对象的build创建原生组件树。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。 194 195自定义组件的更新遵循[状态管理](../quick-start/arkts-state-management-overview.md)的更新机制。WrappedBuilder中直接使用的自定义组件其父组件为BuilderNode对象。因此,更新子组件即WrappedBuilder中定义的自定义组件,需要遵循状态管理的定义将相关的状态变量定义为[\@Prop](../quick-start/arkts-prop.md)或者[\@ObjectLink](../quick-start/arkts-observed-and-objectlink.md)。装饰器的选择请参照状态管理的装饰器规格结合应用开发需求进行选择。 196 197 198使用update更新BuilderNode中的节点。 199 200使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)触发BuilderNode中节点的全量更新。 201 202更新BuilderNode中的节点。 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// 自定义组件 215@Component 216struct TextBuilder { 217 // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop 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 }) // 自定义组件 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 // 调用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## 解除实体节点引用关系 297 298由于BuilderNode对应的是后端的实体节点,正常的内存释放依赖前端对象的回收。如果期望直接释放后端的节点对象,则可以通过调用[dispose](../reference/apis-arkui/js-apis-arkui-builderNode.md#dispose12)与实体节点解除引用关系,此时持有的前端BuilderNode对象不再影响实体节点的生命周期。 299 300> **说明:** 301> 302> 当BuilderNode对象调用dispose之后,不仅BuilderNode对象与后端实体节点解除引用关系,BuilderNode中的FrameNode与RenderNode也会同步和实体节点解除引用关系。 303 304## 注入触摸事件 305 306BuilderNode中提供了[postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent),可以通过该接口向BuilderNode中绑定的组件注入[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md#触摸事件),实现事件的模拟转发。 307 308通过postTouchEvent向BuilderNode对应的节点树中注入触摸事件。 309 310向BuilderNode中的Column组件转发另一个Column的接收事件,即点击下方的Column组件,上方的Colum组件也会收到同样的触摸事件。当Button中的事件被成功识别的时候,返回值为true。 311 312```ts 313import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI'; 314 315class Params { 316 text: string = "this is a text"; 317} 318 319@Builder 320function ButtonBuilder(params: Params) { 321 Column() { 322 Button(`button ` + params.text) 323 .borderWidth(2) 324 .backgroundColor(Color.Orange) 325 .width("100%") 326 .height("100%") 327 .gesture( 328 TapGesture() 329 .onAction((event: GestureEvent) => { 330 console.log("TapGesture"); 331 }) 332 ) 333 } 334 .width(500) 335 .height(300) 336 .backgroundColor(Color.Gray) 337} 338 339class MyNodeController extends NodeController { 340 private rootNode: BuilderNode<[Params]> | null = null; 341 private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder); 342 343 makeNode(uiContext: UIContext): FrameNode | null { 344 this.rootNode = new BuilderNode(uiContext); 345 this.rootNode.build(this.wrapBuilder, { text: "this is a string" }) 346 return this.rootNode.getFrameNode(); 347 } 348 349 postTouchEvent(touchEvent: TouchEvent): void { 350 if (this.rootNode == null) { 351 return; 352 } 353 let result = this.rootNode.postTouchEvent(touchEvent); 354 console.log("result " + result); 355 } 356} 357 358@Entry 359@Component 360struct MyComponent { 361 private nodeController: MyNodeController = new MyNodeController(); 362 363 build() { 364 Column() { 365 NodeContainer(this.nodeController) 366 .height(300) 367 .width(500) 368 Column() 369 .width(500) 370 .height(300) 371 .backgroundColor(Color.Pink) 372 .onTouch((event) => { 373 if (event != undefined) { 374 this.nodeController.postTouchEvent(event); 375 } 376 }) 377 } 378 } 379} 380``` 381 382## 节点复用能力 383 384将[reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12)事件和[recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12)事件传递至BuilderNode中的自定义组件,以实现BuilderNode节点的复用。 385 386```ts 387import { FrameNode,NodeController,BuilderNode,UIContext } from "@kit.ArkUI"; 388 389class MyDataSource { 390 private dataArray: string[] = []; 391 private listener: DataChangeListener | null = null 392 393 public totalCount(): number { 394 return this.dataArray.length; 395 } 396 397 public getData(index: number) { 398 return this.dataArray[index]; 399 } 400 401 public pushData(data: string) { 402 this.dataArray.push(data); 403 } 404 405 public reloadListener(): void { 406 this.listener?.onDataReloaded(); 407 } 408 409 public registerDataChangeListener(listener: DataChangeListener): void { 410 this.listener = listener; 411 } 412 413 public unregisterDataChangeListener(): void { 414 this.listener = null; 415 } 416} 417 418class Params { 419 item: string = ''; 420 421 constructor(item: string) { 422 this.item = item; 423 } 424} 425 426@Builder 427function buildNode(param: Params = new Params("hello")) { 428 ReusableChildComponent2({ item: param.item }); 429} 430 431class MyNodeController extends NodeController { 432 public builderNode: BuilderNode<[Params]> | null = null; 433 public item: string = ""; 434 435 makeNode(uiContext: UIContext): FrameNode | null { 436 if (this.builderNode == null) { 437 this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } }); 438 this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item)); 439 } 440 return this.builderNode.getFrameNode(); 441 } 442} 443 444@Reusable 445@Component 446struct ReusableChildComponent { 447 @State item: string = ''; 448 private controller: MyNodeController = new MyNodeController(); 449 450 aboutToAppear() { 451 this.controller.item = this.item; 452 } 453 454 aboutToRecycle(): void { 455 console.log("ReusableChildComponent aboutToRecycle " + this.item); 456 this.controller?.builderNode?.recycle(); 457 } 458 459 aboutToReuse(params: object): void { 460 console.log("ReusableChildComponent aboutToReuse " + JSON.stringify(params)); 461 this.controller?.builderNode?.reuse(params); 462 } 463 464 build() { 465 NodeContainer(this.controller); 466 } 467} 468 469@Component 470struct ReusableChildComponent2 { 471 @Prop item: string = "false"; 472 473 aboutToReuse(params: Record<string, object>) { 474 console.log("ReusableChildComponent2 Reusable 2 " + JSON.stringify(params)); 475 } 476 477 aboutToRecycle(): void { 478 console.log("ReusableChildComponent2 aboutToRecycle 2 " + this.item); 479 } 480 481 build() { 482 Row() { 483 Text(this.item) 484 .fontSize(20) 485 .backgroundColor(Color.Yellow) 486 .margin({ left: 10 }) 487 }.margin({ left: 10, right: 10 }) 488 } 489} 490 491 492@Entry 493@Component 494struct Index { 495 @State data: MyDataSource = new MyDataSource(); 496 497 aboutToAppear() { 498 for (let i = 0;i < 100; i++) { 499 this.data.pushData(i.toString()); 500 } 501 } 502 503 build() { 504 Column() { 505 List({ space: 3 }) { 506 LazyForEach(this.data, (item: string) => { 507 ListItem() { 508 ReusableChildComponent({ item: item }) 509 } 510 }, (item: string) => item) 511 } 512 .width('100%') 513 .height('100%') 514 } 515 } 516} 517``` 518 519## 通过系统环境变化更新节点 520 521使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)来监听[系统环境变化](../reference/apis-ability-kit/js-apis-app-ability-configuration.md)事件,以触发节点的全量更新。 522 523> **说明:** 524> 525> updateConfiguration接口用于通知对象进行更新,更新所使用的系统环境取决于应用当前系统环境的变化。 526 527```ts 528import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 529import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit'; 530 531class Params { 532 text: string = "" 533 534 constructor(text: string) { 535 this.text = text; 536 } 537} 538 539// 自定义组件 540@Component 541struct TextBuilder { 542 // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop 543 @Prop message: string = "TextBuilder"; 544 545 build() { 546 Row() { 547 Column() { 548 Text(this.message) 549 .fontSize(50) 550 .fontWeight(FontWeight.Bold) 551 .margin({ bottom: 36 }) 552 .fontColor($r(`app.color.text_color`)) 553 .backgroundColor($r(`app.color.start_window_background`)) 554 } 555 } 556 } 557} 558 559@Builder 560function buildText(params: Params) { 561 Column() { 562 Text(params.text) 563 .fontSize(50) 564 .fontWeight(FontWeight.Bold) 565 .margin({ bottom: 36 }) 566 .fontColor($r(`app.color.text_color`)) 567 TextBuilder({ message: params.text }) // 自定义组件 568 }.backgroundColor($r(`app.color.start_window_background`)) 569} 570 571class TextNodeController extends NodeController { 572 private textNode: BuilderNode<[Params]> | null = null; 573 private message: string = ""; 574 575 constructor(message: string) { 576 super() 577 this.message = message; 578 } 579 580 makeNode(context: UIContext): FrameNode | null { 581 return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null; 582 } 583 584 createNode(context: UIContext) { 585 this.textNode = new BuilderNode(context); 586 this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message)); 587 builderNodeMap.push(this.textNode); 588 } 589 590 deleteNode() { 591 let node = builderNodeMap.pop(); 592 node?.dispose(); 593 } 594 595 update(message: string) { 596 if (this.textNode !== null) { 597 // 调用update进行更新。 598 this.textNode.update(new Params(message)); 599 } 600 } 601} 602 603// 记录创建的自定义节点对象 604const builderNodeMap: Array<BuilderNode<[Params]>> = new Array(); 605 606function updateColorMode() { 607 builderNodeMap.forEach((value, index) => { 608 // 通知BuilderNode环境变量改变 609 value.updateConfiguration(); 610 }) 611} 612 613@Entry 614@Component 615struct Index { 616 @State message: string = "hello" 617 private textNodeController: TextNodeController = new TextNodeController(this.message); 618 private count = 0; 619 620 aboutToAppear(): void { 621 let environmentCallback: EnvironmentCallback = { 622 onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => { 623 console.log('onMemoryLevel'); 624 }, 625 onConfigurationUpdated: (config: Configuration): void => { 626 console.log('onConfigurationUpdated ' + JSON.stringify(config)); 627 updateColorMode(); 628 } 629 } 630 // 注册监听回调 631 this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback); 632 //创建自定义节点并添加至map 633 this.textNodeController.createNode(this.getUIContext()); 634 } 635 636 aboutToDisappear(): void { 637 //移除map中的引用,并将自定义节点释放 638 this.textNodeController.deleteNode(); 639 } 640 641 build() { 642 Row() { 643 Column() { 644 NodeContainer(this.textNodeController) 645 .width('100%') 646 .height(200) 647 .backgroundColor('#FFF0F0F0') 648 Button('Update') 649 .onClick(() => { 650 this.count += 1; 651 const message = "Update " + this.count.toString(); 652 this.textNodeController.update(message); 653 }) 654 } 655 .width('100%') 656 .height('100%') 657 } 658 .height('100%') 659 } 660} 661``` 662 663## 跨页面复用注意事项 664 665在使用[路由](../reference/apis-arkui/js-apis-router.md)接口[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)、[router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10)操作页面时,若某个被缓存的BuilderNode位于即将销毁的页面内,那么在新页面中复用该BuilderNode时,可能会存在数据无法更新或新创建节点无法显示的问题。以[router.replaceNamedRoute](../reference/apis-arkui/js-apis-router.md#routerreplacenamedroute10)为例,在以下示例代码中,当点击“router replace”按钮后,页面将切换至PageTwo,同时标志位isShowText会被设定为false。 666 667```ts 668// ets/pages/Index.ets 669import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 670import "ets/pages/PageTwo" 671 672@Builder 673function buildText() { 674 // @Builder中使用语法节点生成BuilderProxyNode 675 if (true) { 676 MyComponent() 677 } 678} 679 680@Component 681struct MyComponent { 682 @StorageLink("isShowText") isShowText: boolean = true; 683 684 build() { 685 if (this.isShowText) { 686 Column() { 687 Text("BuilderNode Reuse") 688 .fontSize(36) 689 .fontWeight(FontWeight.Bold) 690 .padding(16) 691 } 692 } 693 } 694} 695 696class TextNodeController extends NodeController { 697 private rootNode: FrameNode | null = null; 698 private textNode: BuilderNode<[]> | null = null; 699 700 makeNode(context: UIContext): FrameNode | null { 701 this.rootNode = new FrameNode(context); 702 703 if (AppStorage.has("textNode")) { 704 // 复用AppStorage中的BuilderNode 705 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 706 const parent = this.textNode.getFrameNode()?.getParent(); 707 if (parent) { 708 parent.removeChild(this.textNode.getFrameNode()); 709 } 710 } else { 711 this.textNode = new BuilderNode(context); 712 this.textNode.build(wrapBuilder<[]>(buildText)); 713 // 将创建的BuilderNode存入AppStorage 714 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 715 } 716 this.rootNode.appendChild(this.textNode.getFrameNode()); 717 718 return this.rootNode; 719 } 720} 721 722@Entry({ routeName: "myIndex" }) 723@Component 724struct Index { 725 aboutToAppear(): void { 726 AppStorage.setOrCreate<boolean>("isShowText", true); 727 } 728 729 build() { 730 Row() { 731 Column() { 732 NodeContainer(new TextNodeController()) 733 .width('100%') 734 .backgroundColor('#FFF0F0F0') 735 Button('Router pageTwo') 736 .onClick(() => { 737 // 改变AppStorage中的状态变量触发Text节点的重新创建 738 AppStorage.setOrCreate<boolean>("isShowText", false); 739 740 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 741 }) 742 .margin({ top: 16 }) 743 } 744 .width('100%') 745 .height('100%') 746 .padding(16) 747 } 748 .height('100%') 749 } 750} 751``` 752 753PageTwo的实现如下: 754 755```ts 756// ets/pages/PageTwo.ets 757// 该页面中存在一个按钮,可跳转回主页面,回到主页面后,原有的文字消失 758import "ets/pages/Index" 759 760@Entry({ routeName: "pageTwo" }) 761@Component 762struct PageTwo { 763 build() { 764 Column() { 765 Button('Router replace to index') 766 .onClick(() => { 767 this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" }); 768 }) 769 } 770 .height('100%') 771 .width('100%') 772 .alignItems(HorizontalAlign.Center) 773 .padding(16) 774 } 775} 776``` 777 778 779 780在API version 16之前,解决该问题的方法是在页面销毁时,将页面上的BuilderNode从缓存中移除。以上述例子为例,可以在页面跳转前,通过点击事件将BuilderNode从AppStorage中移除,以此达到预期效果。 781 782API version 16及之后版本,BuilderNode在新页面被复用时,会自动刷新自身内容,无需在页面销毁时将BuilderNode从缓存中移除。 783 784```ts 785// ets/pages/Index.ets 786import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI"; 787import "ets/pages/PageTwo" 788 789@Builder 790function buildText() { 791 // @Builder中使用语法节点生成BuilderProxyNode 792 if (true) { 793 MyComponent() 794 } 795} 796 797@Component 798struct MyComponent { 799 @StorageLink("isShowText") isShowText: boolean = true; 800 801 build() { 802 if (this.isShowText) { 803 Column() { 804 Text("BuilderNode Reuse") 805 .fontSize(36) 806 .fontWeight(FontWeight.Bold) 807 .padding(16) 808 } 809 } 810 } 811} 812 813class TextNodeController extends NodeController { 814 private rootNode: FrameNode | null = null; 815 private textNode: BuilderNode<[]> | null = null; 816 817 makeNode(context: UIContext): FrameNode | null { 818 this.rootNode = new FrameNode(context); 819 820 if (AppStorage.has("textNode")) { 821 // 复用AppStorage中的BuilderNode 822 this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>; 823 const parent = this.textNode.getFrameNode()?.getParent(); 824 if (parent) { 825 parent.removeChild(this.textNode.getFrameNode()); 826 } 827 } else { 828 this.textNode = new BuilderNode(context); 829 this.textNode.build(wrapBuilder<[]>(buildText)); 830 // 将创建的BuilderNode存入AppStorage 831 AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode); 832 } 833 this.rootNode.appendChild(this.textNode.getFrameNode()); 834 835 return this.rootNode; 836 } 837} 838 839@Entry({ routeName: "myIndex" }) 840@Component 841struct Index { 842 aboutToAppear(): void { 843 AppStorage.setOrCreate<boolean>("isShowText", true); 844 } 845 846 build() { 847 Row() { 848 Column() { 849 NodeContainer(new TextNodeController()) 850 .width('100%') 851 .backgroundColor('#FFF0F0F0') 852 Button('Router pageTwo') 853 .onClick(() => { 854 // 改变AppStorage中的状态变量触发Text节点的重新创建 855 AppStorage.setOrCreate<boolean>("isShowText", false); 856 // 将BuilderNode从AppStorage中移除 857 AppStorage.delete("textNode"); 858 859 this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" }); 860 }) 861 .margin({ top: 16 }) 862 } 863 .width('100%') 864 .height('100%') 865 .padding(16) 866 } 867 .height('100%') 868 } 869} 870``` 871