1# 自定义声明式节点 (BuilderNode) 2 3## 概述 4 5自定义声明式节点 ([BuilderNode](../reference/apis-arkui/js-apis-arkui-builderNode.md))提供能够挂载系统组件的能力,支持采用无状态的UI方式,通过[全局自定义构建函数](../ui/state-management/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](../ui/state-management/arkts-wrapBuilder.md)包装的[全局自定义构建函数](../ui/state-management/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](../ui/state-management/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自定义组件的更新遵循[状态管理](../ui/state-management/arkts-state-management-overview.md)的更新机制。WrappedBuilder中直接使用的自定义组件其父组件为BuilderNode对象。因此,更新子组件即WrappedBuilder中定义的自定义组件,需要遵循状态管理的定义将相关的状态变量定义为[\@Prop](../ui/state-management/arkts-prop.md)或者[\@ObjectLink](../ui/state-management/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> 若前端对象BuilderNode无法释放,容易导致内存泄漏。建议在不再需要对该BuilderNode对象进行操作时,开发者应主动调用dispose释放后端节点,以减少引用关系的复杂性,降低内存泄漏的风险。 305 306## 注入触摸事件 307 308BuilderNode中提供了[postTouchEvent](../reference/apis-arkui/js-apis-arkui-builderNode.md#posttouchevent),可以通过该接口向BuilderNode中绑定的组件注入[触摸事件](../reference/apis-arkui/arkui-ts/ts-universal-events-touch.md#触摸事件),实现事件的模拟转发。 309 310通过postTouchEvent向BuilderNode对应的节点树中注入触摸事件。 311 312向BuilderNode中的Column组件转发另一个Column的接收事件,即点击下方的Column组件,上方的Colum组件也会收到同样的触摸事件。当Button中的事件被成功识别的时候,返回值为true。 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## 节点复用能力 385 386调用[reuse](../reference/apis-arkui/js-apis-arkui-builderNode.md#reuse12)接口和[recycle](../reference/apis-arkui/js-apis-arkui-builderNode.md#recycle12)接口,将复用和回收事件传递至BuilderNode中的自定义组件,以实现BuilderNode节点内部的自定义组件的复用。 387 388以下面的Demo为例,被复用的自定义组件ReusableChildComponent可以传递复用和回收事件到其下的自定义组件ReusableChildComponent3,但无法传递给自定义组件ReusableChildComponent2,因为被BuilderNode所隔断。因此需要主动调用BuilderNode的reuse和recycle接口,将复用和回收事件传递给自定义组件ReusableChildComponent2,以达成复用效果。 389 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 }) //该自定义组件在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// 被回收复用的自定义组件,其状态变量会更新,而子自定义组件ReusableChildComponent3中的状态变量也会更新,但BuilderNode会阻断这一传递过程 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 // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ReusableChildComponent2,完成复用 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 // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ReusableChildComponent2,完成复用 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' // 将open改为close可观察到,BuilderNode不通过reuse和recycle接口传递复用时,BuilderNode内部的自定义组件的行为表现 558 }) 559 } 560 }, (item: string) => item) 561 } 562 .width('100%') 563 .height('100%') 564 } 565 } 566} 567``` 568 569 570## 通过系统环境变化更新节点 571 572使用[updateConfiguration](../reference/apis-arkui/js-apis-arkui-builderNode.md#updateconfiguration12)来监听[系统环境变化](../reference/apis-ability-kit/js-apis-app-ability-configuration.md)事件,以触发节点的全量更新。 573 574> **说明:** 575> 576> updateConfiguration接口用于通知对象进行更新,更新所使用的系统环境取决于应用当前系统环境的变化。 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// 自定义组件 591@Component 592struct TextBuilder { 593 // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop 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 }) // 自定义组件 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 // 调用update进行更新。 649 this.textNode.update(new Params(message)); 650 } 651 } 652} 653 654// 记录创建的自定义节点对象 655const builderNodeMap: Array<BuilderNode<[Params]>> = new Array(); 656 657function updateColorMode() { 658 builderNodeMap.forEach((value, index) => { 659 // 通知BuilderNode环境变量改变 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 // 注册监听回调 682 this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback); 683 //创建自定义节点并添加至map 684 this.textNodeController.createNode(this.getUIContext()); 685 } 686 687 aboutToDisappear(): void { 688 //移除map中的引用,并将自定义节点释放 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## 跨页面复用注意事项 715 716在使用[路由](../reference/apis-arkui/js-apis-arkui-UIContext.md#router)接口[router.replaceUrl](../reference/apis-arkui/js-apis-arkui-UIContext.md#replaceurl)、[router.back](../reference/apis-arkui/js-apis-arkui-UIContext.md#back)、[router.clear](../reference/apis-arkui/js-apis-arkui-UIContext.md#clear)、[router.replaceNamedRoute](../reference/apis-arkui/js-apis-arkui-UIContext.md#replacenamedroute)操作页面时,若某个被缓存的BuilderNode位于即将销毁的页面内,那么在新页面中复用该BuilderNode时,可能会存在数据无法更新或新创建节点无法显示的问题。以[router.replaceNamedRoute](../reference/apis-arkui/js-apis-arkui-UIContext.md#replacenamedroute)为例,在以下示例代码中,当点击“router replace”按钮后,页面将切换至PageTwo,同时标志位isShowText会被设定为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 // @Builder中使用语法节点生成BuilderProxyNode 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 // 复用AppStorage中的BuilderNode 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 // 将创建的BuilderNode存入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 // 改变AppStorage中的状态变量触发Text节点的重新创建 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 804PageTwo的实现如下: 805 806```ts 807// ets/pages/PageTwo.ets 808// 该页面中存在一个按钮,可跳转回主页面,回到主页面后,原有的文字消失 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 830 831在API version 16之前,解决该问题的方法是在页面销毁时,将页面上的BuilderNode从缓存中移除。以上述例子为例,可以在页面跳转前,通过点击事件将BuilderNode从AppStorage中移除,以此达到预期效果。 832 833API version 16及之后版本,BuilderNode在新页面被复用时,会自动刷新自身内容,无需在页面销毁时将BuilderNode从缓存中移除。 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 // @Builder中使用语法节点生成BuilderProxyNode 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 // 复用AppStorage中的BuilderNode 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 // 将创建的BuilderNode存入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 // 改变AppStorage中的状态变量触发Text节点的重新创建 906 AppStorage.setOrCreate<boolean>("isShowText", false); 907 // 将BuilderNode从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## BuilderNode中使用LocalStorage 925 926从API version 12开始,自定义组件支持接收[LocalStorage](../ui/state-management/arkts-localstorage.md)实例。可以通过[传递LocalStorage实例](../ui/state-management/arkts-localstorage.md#自定义组件接收localstorage实例)来使用LocalStorage相关的装饰器[@LocalStorageProp](../ui/state-management/arkts-localstorage.md#localstorageprop)、[@LocalStorageLink](../ui/state-management/arkts-localstorage.md#localstoragelink)。 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',和localStorage1中'PropA'的双向同步 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 // 使用LocalStorage 实例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 //构造过程中传递localStorage 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 //构造过程中传递localStorage 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',和localStorage2中'PropB'的双向同步,如果localStorage2中没有'PropB',则使用默认值'Hello World' 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