自定义声明式节点 (BuilderNode)

概述

自定义声明式节点 (BuilderNode)提供能够挂载系统组件的能力,支持采用无状态的UI方式,通过全局自定义构建函数@Builder定制组件树。组件树的根FrameNode节点可通过getFrameNode获取,该节点既可直接由NodeController返回并挂载于NodeContainer节点下,亦可在FrameNode树与RenderNode树中嵌入声明式组件,实现混合显示。同时,BuilderNode具备纹理导出功能,导出的纹理可在XComponent中实现同层渲染。

由BuilderNode构建的ArkTS组件树,支持与自定义节点(如FrameNode、RenderNode)关联使用,确保了系统组件与自定义节点的混合显示效果。对于需与自定义节点对接的第三方框架,BuilderNode提供了嵌入系统组件的方法。

此外,BuilderNode还提供了组件预创建的能力,能够自定义系统组件的创建开始的时间,在后续业务中实现动态挂载与显示。此功能尤其适用于初始化耗时较长的声明式组件,如WebXComponent等,通过预创建,可以有效减少初始化时间,优化组件加载效率。

zh-cn_image_builder-node

基本概念

  • 系统组件:组件是UI的必要元素,形成了在界面中的样子,由ArkUI直接提供的称为系统组件

  • 实体节点:由后端创建的Native节点。

BuilderNode仅可作为叶子节点进行使用。如有更新需要,建议通过BuilderNode中的update方式触发更新,不建议通过BuilderNode中获取的RenderNode对节点进行修改操作。

说明:

  • BuilderNode只支持一个由wrapBuilder包装的全局自定义构建函数@Builder。

  • 一个新建的BuilderNode在build之后才能通过getFrameNode获取到一个指向根节点的FrameNode对象,否则返回null。

  • 如果传入的Builder的根节点为语法节点(if/else/foreach/...),需要额外生成一个FrameNode,在节点树中的显示为“BuilderProxyNode”。

  • 如果BuilderNode通过getFrameNode将节点挂载在另一个FrameNode上,或者将其作为子节点挂载在NodeContainer节点上。则节点中使用父组件的布局约束进行布局。

  • 如果BuilderNode的FrameNode通过getRenderNode形式将自己的节点挂载在RenderNode节点上,由于其FrameNode未上树,其大小默认为0,需要通过构造函数中的selfIdeaSize显式指定布局约束大小,才能正常显示。

  • BuilderNode的预加载并不会减少组件的创建时间。Web组件创建的时候需要在内核中加载资源,预创建不能减少Web组件的创建的时间,但是可以让内核进行预加载,减少正式使用时候内核的加载耗时。

创建BuilderNode对象

BuilderNode对象为一个模板类,需要在创建的时候指定类型。该类型需要与后续build方法中传入的WrappedBuilder的类型保持一致,否则会存在编译告警导致编译失败。

创建组件树

通过BuilderNode的build可以实现组件树的创建。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。

说明:

无状态的UI方法全局@Builder最多拥有一个根节点。

build方法中对应的@Builder支持一个参数作为入参。

build中对于@Builder嵌套@Builder进行使用的场景,需要保证嵌套的参数与build的中提供的入参一致。

对于@Builder嵌套@Builder进行使用的场景,如果入参类型不一致,则要求增加BuilderOptions字段作为build的入参。

需要操作BuilderNode中的对象时,需要保证其引用不被回收。当BuilderNode对象被虚拟机回收之后,它的FrameNode、RenderNode对象也会与后端节点解引用。即从BuilderNode中获取的FrameNode对象不对应任何一个节点。

创建离线节点以及组件树,结合FrameNode进行使用。

BuilderNode的根节点直接作为NodeControllermakeNode返回值。

import { BuilderNode, FrameNode, NodeController, UIContext } from '@kit.ArkUI';

class Params {
  text: string = "";

  constructor(text: string) {
    this.text = text;
  }
}

@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
  }
}

class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = "DEFAULT";

  constructor(message: string) {
    super();
    this.message = message;
  }

  makeNode(context: UIContext): FrameNode | null {
    this.textNode = new BuilderNode(context);
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
    return this.textNode.getFrameNode();
  }
}

@Entry
@Component
struct Index {
  @State message: string = "hello";

  build() {
    Row() {
      Column() {
        NodeContainer(new TextNodeController(this.message))
          .width('100%')
          .height(100)
          .backgroundColor('#FFF0F0F0')
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}

将BuilderNode与RenderNode进行结合使用。

BuilderNode的RenderNode挂载其它RenderNode下时,需要明确定义selfIdeaSize的大小作为BuilderNode的布局约束。不推荐通过该方式挂载节点。

import { NodeController, BuilderNode, FrameNode, UIContext, RenderNode } from "@kit.ArkUI";

class Params {
  text: string = "";

  constructor(text: string) {
    this.text = text;
  }
}

@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
  }
}

class TextNodeController extends NodeController {
  private rootNode: FrameNode | null = null;
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = "DEFAULT";

  constructor(message: string) {
    super();
    this.message = message;
  }

  makeNode(context: UIContext): FrameNode | null {
    this.rootNode = new FrameNode(context);
    let renderNode = new RenderNode();
    renderNode.clipToFrame = false;
    this.textNode = new BuilderNode(context, { selfIdealSize: { width: 150, height: 150 } });
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
    const textRenderNode = this.textNode?.getFrameNode()?.getRenderNode();

    const rootRenderNode = this.rootNode.getRenderNode();
    if (rootRenderNode !== null) {
      rootRenderNode.appendChild(renderNode);
      renderNode.appendChild(textRenderNode);
    }

    return this.rootNode;
  }
}

@Entry
@Component
struct Index {
  @State message: string = "hello";

  build() {
    Row() {
      Column() {
        NodeContainer(new TextNodeController(this.message))
          .width('100%')
          .height(100)
          .backgroundColor('#FFF0F0F0')
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}

更新组件树

通过BuilderNode对象的build创建组件树。依照传入的WrappedBuilder对象创建组件树,并持有组件树的根节点。

自定义组件的更新遵循状态管理的更新机制。WrappedBuilder中直接使用的自定义组件其父组件为BuilderNode对象。因此,更新子组件即WrappedBuilder中定义的自定义组件,需要遵循状态管理的定义将相关的状态变量定义为@Prop或者@ObjectLink。装饰器的选择请参照状态管理的装饰器规格结合应用开发需求进行选择。

使用update更新BuilderNode中的节点。

使用updateConfiguration触发BuilderNode中节点的全量更新。

更新BuilderNode中的节点。

import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";

class Params {
  text: string = "";
  constructor(text: string) {
    this.text = text;
  }
}

// 自定义组件
@Component
struct TextBuilder {
  // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop
  @Prop message: string = "TextBuilder";

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 36 })
          .backgroundColor(Color.Gray)
      }
    }
  }
}

@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
    TextBuilder({ message: params.text }) // 自定义组件
  }
}

class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = "";

  constructor(message: string) {
    super()
    this.message = message
  }

  makeNode(context: UIContext): FrameNode | null {
    this.textNode = new BuilderNode(context);
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message))
    return this.textNode.getFrameNode();
  }

  update(message: string) {
    if (this.textNode !== null) {
      // 调用update进行更新。
      this.textNode.update(new Params(message));
    }
  }
}

@Entry
@Component
struct Index {
  @State message: string = "hello";
  private textNodeController: TextNodeController = new TextNodeController(this.message);
  private count = 0;

  build() {
    Row() {
      Column() {
        NodeContainer(this.textNodeController)
          .width('100%')
          .height(200)
          .backgroundColor('#FFF0F0F0')
        Button('Update')
          .onClick(() => {
            this.count += 1;
            const message = "Update " + this.count.toString();
            this.textNodeController.update(message);
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}

解除实体节点引用关系

由于BuilderNode对应的是后端的实体节点,正常的内存释放依赖前端对象的回收。如果期望直接释放后端的节点对象,则可以通过调用dispose与实体节点解除引用关系,此时持有的前端BuilderNode对象不再影响实体节点的生命周期。

说明:

当BuilderNode对象调用dispose之后,不仅BuilderNode对象与后端实体节点解除引用关系,BuilderNode中的FrameNode与RenderNode也会同步和实体节点解除引用关系。

若前端对象BuilderNode无法释放,容易导致内存泄漏。建议在不再需要对该BuilderNode对象进行操作时,开发者应主动调用dispose释放后端节点,以减少引用关系的复杂性,降低内存泄漏的风险。

注入触摸事件

BuilderNode中提供了postTouchEvent,可以通过该接口向BuilderNode中绑定的组件注入触摸事件,实现事件的模拟转发。

通过postTouchEvent向BuilderNode对应的节点树中注入触摸事件。

向BuilderNode中的Column组件转发另一个Column接收的事件,即点击下方的Column组件,上方的Column组件也会收到同样的触摸事件。当Button中的事件被成功识别的时候,返回值为true。

import { NodeController, BuilderNode, FrameNode, UIContext } from '@kit.ArkUI';

class Params {
  text: string = "this is a text";
}

@Builder
function ButtonBuilder(params: Params) {
  Column() {
    Button(`button ` + params.text)
      .borderWidth(2)
      .backgroundColor(Color.Orange)
      .width("100%")
      .height("100%")
      .gesture(
        TapGesture()
          .onAction((event: GestureEvent) => {
            console.log("TapGesture");
          })
      )
  }
  .width(500)
  .height(300)
  .backgroundColor(Color.Gray)
}

class MyNodeController extends NodeController {
  private rootNode: BuilderNode<[Params]> | null = null;
  private wrapBuilder: WrappedBuilder<[Params]> = wrapBuilder(ButtonBuilder);

  makeNode(uiContext: UIContext): FrameNode | null {
    this.rootNode = new BuilderNode(uiContext);
    this.rootNode.build(this.wrapBuilder, { text: "this is a string" })
    return this.rootNode.getFrameNode();
  }

  postTouchEvent(touchEvent: TouchEvent): void {
    if (this.rootNode == null) {
      return;
    }
    let result = this.rootNode.postTouchEvent(touchEvent);
    console.log("result " + result);
  }
}

@Entry
@Component
struct MyComponent {
  private nodeController: MyNodeController = new MyNodeController();

  build() {
    Column() {
      NodeContainer(this.nodeController)
        .height(300)
        .width(500)
      Column()
        .width(500)
        .height(300)
        .backgroundColor(Color.Pink)
        .onTouch((event) => {
          if (event != undefined) {
            this.nodeController.postTouchEvent(event);
          }
        })
    }
  }
}

BuilderNode内的BuilderProxyNode导致树结构发生变化

若传入的Builder的根节点为语法节点(if/else/foreach/…)或自定义组件,将额外生成一个FrameNode,在节点树中显示为“BuilderProxyNode”,这会导致树结构变化,影响某些测试的传递过程。

在以下示例中,Column和Row绑定了触摸事件,同时Column设置了hitTestBehavior属性为HitTestMode.Transparent。然而,由于生成了BuilderProxyNode,且BuilderProxyNode无法设置属性,因此在触摸Column时,Column的触摸测试无法传递到Row上。

BuilderNode_BuilderProxyNode_1

import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI';

@Component
struct BlueRowComponent {
  build() {
    Row() {
      Row() {
      }
      .width('100%')
      .height('200vp')
      .backgroundColor(0xFF2787D9)
      .onTouch((event: TouchEvent) => {
        // 触摸绿色Column,蓝色Row的触摸事件不触发
        console.info("blue touched: " + event.type);
      })
    }
  }
}

@Component
struct GreenColumnComponent {
  build() {
    Column() {
    }
    .width('100%')
    .height('100vp')
    .backgroundColor(0xFF17A98D)
    .hitTestBehavior(HitTestMode.Transparent)
    .onTouch((event: TouchEvent) => {
      console.info("green touched: " + event.type);
    })
  }
}

@Builder
function buildBlueRow() {
  // Builder直接挂载自定义组件,生成BuilderProxyNode
  BlueRowComponent()
}

@Builder
function buildGreenColumn() {
  // Builder直接挂载自定义组件,生成BuilderProxyNode
  GreenColumnComponent()
}

class MyNodeController extends NodeController {
  makeNode(uiContext: UIContext): FrameNode | null {
    const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer');

    const blueRowNode = new BuilderNode(uiContext);
    blueRowNode.build(wrapBuilder(buildBlueRow));

    const greenColumnNode = new BuilderNode(uiContext);
    greenColumnNode.build(wrapBuilder(buildGreenColumn));

    // greenColumnNode覆盖在blueRowNode上
    relativeContainer.appendChild(blueRowNode.getFrameNode());
    relativeContainer.appendChild(greenColumnNode.getFrameNode());

    return relativeContainer;
  }
}

@Entry
@Component
struct Index {
  build() {
    Column() {
      NodeContainer(new MyNodeController())
    }
  }
}

在上述场景中,若要实现触摸测试的传递,可以使用一个容器组件包裹语法节点或自定义组件,以避免生成BuilderProxyNode,并将容器组件的hitTestBehavior设置为HitTestMode.Transparent,从而向兄弟节点传递触摸测试。

BuilderNode_BuilderProxyNode_2

import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI';

@Component
struct BlueRowComponent {
  build() {
    Row() {
      Row() {
      }
      .width('100%')
      .height('200vp')
      .backgroundColor(0xFF2787D9)
      .onTouch((event: TouchEvent) => {
        // 触摸绿色Column,蓝色Row的触摸事件触发
        console.info("blue touched: " + event.type);
      })
    }
  }
}

@Component
struct GreenColumnComponent {
  build() {
    Column() {
    }
    .width('100%')
    .height('100vp')
    .backgroundColor(0xFF17A98D)
    .hitTestBehavior(HitTestMode.Transparent)
    .onTouch((event: TouchEvent) => {
      console.info("green touched: " + event.type);
    })
  }
}

@Builder
function buildBlueRow() {
  // Builder直接挂载自定义组件,生成BuilderProxyNode
  BlueRowComponent()
}

@Builder
function buildGreenColumn() {
  // Builder根节点为容器组件,不会生成BuilderProxyNode,可以设置属性
  Stack() {
    GreenColumnComponent()
  }
  .hitTestBehavior(HitTestMode.Transparent)
}

class MyNodeController extends NodeController {
  makeNode(uiContext: UIContext): FrameNode | null {
    const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer');

    const blueRowNode = new BuilderNode(uiContext);
    blueRowNode.build(wrapBuilder(buildBlueRow));

    const greenColumnNode = new BuilderNode(uiContext);
    greenColumnNode.build(wrapBuilder(buildGreenColumn));

    // greenColumnNode覆盖在blueRowNode上
    relativeContainer.appendChild(blueRowNode.getFrameNode());
    relativeContainer.appendChild(greenColumnNode.getFrameNode());

    return relativeContainer;
  }
}

@Entry
@Component
struct Index {
  build() {
    Column() {
      NodeContainer(new MyNodeController())
    }
  }
}

此外,对于自定义组件,可以直接设置属性,此时将额外生成节点__Common__,自定义组件的属性将挂载于__Common__上,同样能够实现上述效果。

BuilderNode_BuilderProxyNode_3

import { BuilderNode, typeNode, NodeController, UIContext } from '@kit.ArkUI';

@Component
struct BlueRowComponent {
  build() {
    Row() {
      Row() {
      }
      .width('100%')
      .height('200vp')
      .backgroundColor(0xFF2787D9)
      .onTouch((event: TouchEvent) => {
        // 触摸绿色Column,蓝色Row的触摸事件触发
        console.info("blue touched: " + event.type);
      })
    }
  }
}

@Component
struct GreenColumnComponent {
  build() {
    Column() {
    }
    .width('100%')
    .height('100vp')
    .backgroundColor(0xFF17A98D)
    .hitTestBehavior(HitTestMode.Transparent)
    .onTouch((event: TouchEvent) => {
      console.info("green touched: " + event.type);
    })
  }
}

@Builder
function buildBlueRow() {
  // Builder直接挂载自定义组件,生成BuilderProxyNode
  BlueRowComponent()
}

@Builder
function buildGreenColumn() {
  // 给自定义组件设置属性生成__Common__节点,Builder根节点为__Common__节点,不会生成BuilderProxyNode
  GreenColumnComponent()
    .hitTestBehavior(HitTestMode.Transparent)
}

class MyNodeController extends NodeController {
  makeNode(uiContext: UIContext): FrameNode | null {
    const relativeContainer = typeNode.createNode(uiContext, 'RelativeContainer');

    const blueRowNode = new BuilderNode(uiContext);
    blueRowNode.build(wrapBuilder(buildBlueRow));

    const greenColumnNode = new BuilderNode(uiContext);
    greenColumnNode.build(wrapBuilder(buildGreenColumn));

    // greenColumnNode覆盖在blueRowNode上
    relativeContainer.appendChild(blueRowNode.getFrameNode());
    relativeContainer.appendChild(greenColumnNode.getFrameNode());

    return relativeContainer;
  }
}

@Entry
@Component
struct Index {
  build() {
    Column() {
      NodeContainer(new MyNodeController())
    }
  }
}

BuilderNode调用reuse和recycle接口实现节点复用能力

调用reuse接口和recycle接口,将复用和回收事件传递至BuilderNode中的自定义组件,以实现BuilderNode节点内部的自定义组件的复用。

以下面的Demo为例,被复用的自定义组件ReusableChildComponent可以传递复用和回收事件到其下的自定义组件ChildComponent3,但无法传递给自定义组件ChildComponent2,因为被BuilderNode所隔断。因此需要主动调用BuilderNode的reuse和recycle接口,将复用和回收事件传递给自定义组件ChildComponent2,以达成复用效果。

zh-cn_image_reuse-recycle

import { FrameNode, NodeController, BuilderNode, UIContext } from "@kit.ArkUI";

const TEST_TAG: string = "Reuse+Recycle";

class MyDataSource {
  private dataArray: string[] = [];
  private listener: DataChangeListener | null = null

  public totalCount(): number {
    return this.dataArray.length;
  }

  public getData(index: number) {
    return this.dataArray[index];
  }

  public pushData(data: string) {
    this.dataArray.push(data);
  }

  public reloadListener(): void {
    this.listener?.onDataReloaded();
  }

  public registerDataChangeListener(listener: DataChangeListener): void {
    this.listener = listener;
  }

  public unregisterDataChangeListener(): void {
    this.listener = null;
  }
}

class Params {
  item: string = '';

  constructor(item: string) {
    this.item = item;
  }
}

@Builder
function buildNode(param: Params = new Params("hello")) {
  Row() {
    Text(`C${param.item} -- `)
    ChildComponent2({ item: param.item }) //该自定义组件在BuilderNode中无法被正确复用
  }
}

class MyNodeController extends NodeController {
  public builderNode: BuilderNode<[Params]> | null = null;
  public item: string = "";

  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.builderNode == null) {
      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
    }
    return this.builderNode.getFrameNode();
  }
}

// 被回收复用的自定义组件,其状态变量会更新,而子自定义组件ChildComponent3中的状态变量也会更新,但BuilderNode会阻断这一传递过程
@Reusable
@Component
struct ReusableChildComponent {
  @Prop item: string = '';
  @Prop switch: string = '';
  private controller: MyNodeController = new MyNodeController();

  aboutToAppear() {
    this.controller.item = this.item;
  }

  aboutToRecycle(): void {
    console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`);

    // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ChildComponent2,完成复用
    if (this.switch === 'open') {
      this.controller?.builderNode?.recycle();
    }
  }

  aboutToReuse(params: object): void {
    console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`);

    // 当开关为open,通过BuilderNode的reuse接口和recycle接口传递给其下的自定义组件,例如ChildComponent2,完成复用
    if (this.switch === 'open') {
      this.controller?.builderNode?.reuse(params);
    }
  }

  build() {
    Row() {
      Text(`A${this.item}--`)
      ChildComponent3({ item: this.item })
      NodeContainer(this.controller);
    }
  }
}

@Component
struct ChildComponent2 {
  @Prop item: string = "false";

  aboutToReuse(params: Record<string, object>) {
    console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`);
  }

  aboutToRecycle(): void {
    console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`);
  }

  build() {
    Row() {
      Text(`D${this.item}`)
        .fontSize(20)
        .backgroundColor(Color.Yellow)
        .margin({ left: 10 })
    }.margin({ left: 10, right: 10 })
  }
}

@Component
struct ChildComponent3 {
  @Prop item: string = "false";

  aboutToReuse(params: Record<string, object>) {
    console.log(`${TEST_TAG} ChildComponent3 aboutToReuse ${JSON.stringify(params)}`);
  }

  aboutToRecycle(): void {
    console.log(`${TEST_TAG} ChildComponent3 aboutToRecycle ${this.item}`);
  }

  build() {
    Row() {
      Text(`B${this.item}`)
        .fontSize(20)
        .backgroundColor(Color.Yellow)
        .margin({ left: 10 })
    }.margin({ left: 10, right: 10 })
  }
}


@Entry
@Component
struct Index {
  @State data: MyDataSource = new MyDataSource();

  aboutToAppear() {
    for (let i = 0; i < 100; i++) {
      this.data.pushData(i.toString());
    }
  }

  build() {
    Column() {
      List({ space: 3 }) {
        LazyForEach(this.data, (item: string) => {
          ListItem() {
            ReusableChildComponent({
              item: item,
              switch: 'open' // 将open改为close可观察到,BuilderNode不通过reuse和recycle接口传递复用时,BuilderNode内部的自定义组件的行为表现
            })
          }
        }, (item: string) => item)
      }
      .width('100%')
      .height('100%')
    }
  }
}

BuilderNode在子自定义组件中使用@Reusable装饰器

BuilderNode节点的复用机制与使用@Reusable装饰器的自定义组件的复用机制会相互冲突。因此,当BuilderNode的子节点为自定义组件时,不支持该自定义组件使用@Reusable装饰器标记,否则将导致应用程序触发JSCrash。若需要使用@Reusable装饰器,应使用一个普通自定义组件包裹该自定义组件。

在下面的示例中,ReusableChildComponent作为BuilderNode的子自定义组件,无法标记为@Reusable。通过ChildComponent2对其包裹,ReusableChildComponent可以使用@Reusable装饰器标记。

BuilderNode-Reusable

import { FrameNode, NodeController, BuilderNode, UIContext } from '@kit.ArkUI';

const TEST_TAG: string = "Reusable";

class Params {
  item: string = '';

  constructor(item: string) {
    this.item = item;
  }
}

@Builder
function buildNode(param: Params = new Params("Hello")) {
  ChildComponent2({ item: param.item })
  // 如果直接使用ReusableChildComponent,则会编译报错
  // ReusableChildComponent({ item: param.item })
}

class MyNodeController extends NodeController {
  public builderNode: BuilderNode<[Params]> | null = null;
  public item: string = "";

  constructor(item: string) {
    super();
    this.item = item;
  }

  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.builderNode == null) {
      this.builderNode = new BuilderNode(uiContext, { selfIdealSize: { width: 300, height: 200 } });
      this.builderNode.build(wrapBuilder<[Params]>(buildNode), new Params(this.item));
    }
    return this.builderNode.getFrameNode();
  }
}

// 标记了@Reusable的自定义组件,无法直接被BuilderNode挂载为子节点
@Reusable
@Component
struct ReusableChildComponent {
  @Prop item: string = '';

  aboutToReuse(params: object): void {
    console.log(`${TEST_TAG} ReusableChildComponent aboutToReuse ${JSON.stringify(params)}`);
  }

  aboutToRecycle(): void {
    console.log(`${TEST_TAG} ReusableChildComponent aboutToRecycle ${this.item}`);
  }

  build() {
    Text(`A--${this.item}`)
  }
}

// 未标记@Reusable的自定义组件
@Component
struct ChildComponent2 {
  @Prop item: string = "";

  aboutToReuse(params: Record<string, object>) {
    console.log(`${TEST_TAG} ChildComponent2 aboutToReuse ${JSON.stringify(params)}`);
  }

  aboutToRecycle(): void {
    console.log(`${TEST_TAG} ChildComponent2 aboutToRecycle ${this.item}`);
  }

  build() {
    ReusableChildComponent({ item: this.item })
  }
}


@Entry
@Component
struct Index {
  @State controller: MyNodeController = new MyNodeController("Child");

  build() {
    Column() {
      NodeContainer(this.controller)
    }
    .width('100%')
    .height('100%')
  }
}

通过系统环境变化更新节点

使用updateConfiguration来监听系统环境变化事件,以触发节点的全量更新。

说明:

updateConfiguration接口用于通知对象进行更新,更新所使用的系统环境取决于应用当前系统环境的变化。

import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
import { AbilityConstant, Configuration, EnvironmentCallback } from '@kit.AbilityKit';

class Params {
  text: string = ""

  constructor(text: string) {
    this.text = text;
  }
}

// 自定义组件
@Component
struct TextBuilder {
  // 作为自定义组件中需要更新的属性,数据类型为基础属性,定义为@Prop
  @Prop message: string = "TextBuilder";

  build() {
    Row() {
      Column() {
        Text(this.message)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 36 })
          .fontColor($r(`app.color.text_color`))
          .backgroundColor($r(`app.color.start_window_background`))
      }
    }
  }
}

@Builder
function buildText(params: Params) {
  Column() {
    Text(params.text)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
      .fontColor($r(`app.color.text_color`))
    TextBuilder({ message: params.text }) // 自定义组件
  }.backgroundColor($r(`app.color.start_window_background`))
}

class TextNodeController extends NodeController {
  private textNode: BuilderNode<[Params]> | null = null;
  private message: string = "";

  constructor(message: string) {
    super()
    this.message = message;
  }

  makeNode(context: UIContext): FrameNode | null {
    return this.textNode?.getFrameNode() ? this.textNode?.getFrameNode() : null;
  }

  createNode(context: UIContext) {
    this.textNode = new BuilderNode(context);
    this.textNode.build(wrapBuilder<[Params]>(buildText), new Params(this.message));
    builderNodeMap.push(this.textNode);
  }

  deleteNode() {
    let node = builderNodeMap.pop();
    node?.dispose();
  }

  update(message: string) {
    if (this.textNode !== null) {
      // 调用update进行更新。
      this.textNode.update(new Params(message));
    }
  }
}

// 记录创建的自定义节点对象
const builderNodeMap: Array<BuilderNode<[Params]>> = new Array();

function updateColorMode() {
  builderNodeMap.forEach((value, index) => {
    // 通知BuilderNode环境变量改变
    value.updateConfiguration();
  })
}

@Entry
@Component
struct Index {
  @State message: string = "hello"
  private textNodeController: TextNodeController = new TextNodeController(this.message);
  private count = 0;

  aboutToAppear(): void {
    let environmentCallback: EnvironmentCallback = {
      onMemoryLevel: (level: AbilityConstant.MemoryLevel): void => {
        console.log('onMemoryLevel');
      },
      onConfigurationUpdated: (config: Configuration): void => {
        console.log('onConfigurationUpdated ' + JSON.stringify(config));
        updateColorMode();
      }
    }
    // 注册监听回调
    this.getUIContext().getHostContext()?.getApplicationContext().on('environment', environmentCallback);
    //创建自定义节点并添加至map
    this.textNodeController.createNode(this.getUIContext());
  }

  aboutToDisappear(): void {
    //移除map中的引用,并将自定义节点释放
    this.textNodeController.deleteNode();
  }

  build() {
    Row() {
      Column() {
        NodeContainer(this.textNodeController)
          .width('100%')
          .height(200)
          .backgroundColor('#FFF0F0F0')
        Button('Update')
          .onClick(() => {
            this.count += 1;
            const message = "Update " + this.count.toString();
            this.textNodeController.update(message);
          })
      }
      .width('100%')
      .height('100%')
    }
    .height('100%')
  }
}

跨页面复用注意事项

在使用路由接口router.replaceUrlrouter.backrouter.clearrouter.replaceNamedRoute操作页面时,若某个被缓存的BuilderNode位于即将销毁的页面内,那么在新页面中复用该BuilderNode时,可能会存在数据无法更新或新创建节点无法显示的问题。以router.replaceNamedRoute为例,在以下示例代码中,当点击“router replace”按钮后,页面将切换至PageTwo,同时标志位isShowText会被设定为false。

// ets/pages/Index.ets
import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
import "ets/pages/PageTwo"

@Builder
function buildText() {
  // @Builder中使用语法节点生成BuilderProxyNode
  if (true) {
    MyComponent()
  }
}

@Component
struct MyComponent {
  @StorageLink("isShowText") isShowText: boolean = true;

  build() {
    if (this.isShowText) {
      Column() {
        Text("BuilderNode Reuse")
          .fontSize(36)
          .fontWeight(FontWeight.Bold)
          .padding(16)
      }
    }
  }
}

class TextNodeController extends NodeController {
  private rootNode: FrameNode | null = null;
  private textNode: BuilderNode<[]> | null = null;

  makeNode(context: UIContext): FrameNode | null {
    this.rootNode = new FrameNode(context);

    if (AppStorage.has("textNode")) {
      // 复用AppStorage中的BuilderNode
      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
      const parent = this.textNode.getFrameNode()?.getParent();
      if (parent) {
        parent.removeChild(this.textNode.getFrameNode());
      }
    } else {
      this.textNode = new BuilderNode(context);
      this.textNode.build(wrapBuilder<[]>(buildText));
      // 将创建的BuilderNode存入AppStorage
      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
    }
    this.rootNode.appendChild(this.textNode.getFrameNode());

    return this.rootNode;
  }
}

@Entry({ routeName: "myIndex" })
@Component
struct Index {
  aboutToAppear(): void {
    AppStorage.setOrCreate<boolean>("isShowText", true);
  }

  build() {
    Row() {
      Column() {
        NodeContainer(new TextNodeController())
          .width('100%')
          .backgroundColor('#FFF0F0F0')
        Button('Router pageTwo')
          .onClick(() => {
            // 改变AppStorage中的状态变量触发Text节点的重新创建
            AppStorage.setOrCreate<boolean>("isShowText", false);

            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
          })
          .margin({ top: 16 })
      }
      .width('100%')
      .height('100%')
      .padding(16)
    }
    .height('100%')
  }
}

PageTwo的实现如下:

// ets/pages/PageTwo.ets
// 该页面中存在一个按钮,可跳转回主页面,回到主页面后,原有的文字消失
import "ets/pages/Index"

@Entry({ routeName: "pageTwo" })
@Component
struct PageTwo {
  build() {
    Column() {
      Button('Router replace to index')
        .onClick(() => {
          this.getUIContext().getRouter().replaceNamedRoute({ name: "myIndex" });
        })
    }
    .height('100%')
    .width('100%')
    .alignItems(HorizontalAlign.Center)
    .padding(16)
  }
}

BuilderNode Reuse Example

在API version 16之前,解决该问题的方法是在页面销毁时,将页面上的BuilderNode从缓存中移除。以上述例子为例,可以在页面跳转前,通过点击事件将BuilderNode从AppStorage中移除,以此达到预期效果。

API version 16及之后版本,BuilderNode在新页面被复用时,会自动刷新自身内容,无需在页面销毁时将BuilderNode从缓存中移除。

// ets/pages/Index.ets
import { NodeController, BuilderNode, FrameNode, UIContext } from "@kit.ArkUI";
import "ets/pages/PageTwo"

@Builder
function buildText() {
  // @Builder中使用语法节点生成BuilderProxyNode
  if (true) {
    MyComponent()
  }
}

@Component
struct MyComponent {
  @StorageLink("isShowText") isShowText: boolean = true;

  build() {
    if (this.isShowText) {
      Column() {
        Text("BuilderNode Reuse")
          .fontSize(36)
          .fontWeight(FontWeight.Bold)
          .padding(16)
      }
    }
  }
}

class TextNodeController extends NodeController {
  private rootNode: FrameNode | null = null;
  private textNode: BuilderNode<[]> | null = null;

  makeNode(context: UIContext): FrameNode | null {
    this.rootNode = new FrameNode(context);

    if (AppStorage.has("textNode")) {
      // 复用AppStorage中的BuilderNode
      this.textNode = AppStorage.get<BuilderNode<[]>>("textNode") as BuilderNode<[]>;
      const parent = this.textNode.getFrameNode()?.getParent();
      if (parent) {
        parent.removeChild(this.textNode.getFrameNode());
      }
    } else {
      this.textNode = new BuilderNode(context);
      this.textNode.build(wrapBuilder<[]>(buildText));
      // 将创建的BuilderNode存入AppStorage
      AppStorage.setOrCreate<BuilderNode<[]>>("textNode", this.textNode);
    }
    this.rootNode.appendChild(this.textNode.getFrameNode());

    return this.rootNode;
  }
}

@Entry({ routeName: "myIndex" })
@Component
struct Index {
  aboutToAppear(): void {
    AppStorage.setOrCreate<boolean>("isShowText", true);
  }

  build() {
    Row() {
      Column() {
        NodeContainer(new TextNodeController())
          .width('100%')
          .backgroundColor('#FFF0F0F0')
        Button('Router pageTwo')
          .onClick(() => {
            // 改变AppStorage中的状态变量触发Text节点的重新创建
            AppStorage.setOrCreate<boolean>("isShowText", false);
            // 将BuilderNode从AppStorage中移除
            AppStorage.delete("textNode");

            this.getUIContext().getRouter().replaceNamedRoute({ name: "pageTwo" });
          })
          .margin({ top: 16 })
      }
      .width('100%')
      .height('100%')
      .padding(16)
    }
    .height('100%')
  }
}

BuilderNode中使用LocalStorage

从API version 12开始,自定义组件支持接收LocalStorage实例。可以通过传递LocalStorage实例来使用LocalStorage相关的装饰器@LocalStorageProp@LocalStorageLink

import { BuilderNode, NodeController, UIContext } from '@kit.ArkUI';

let localStorage1: LocalStorage = new LocalStorage();
localStorage1.setOrCreate('PropA', 'PropA');

let localStorage2: LocalStorage = new LocalStorage();
localStorage2.setOrCreate('PropB', 'PropB');

@Entry(localStorage1)
@Component
struct Index {
  // 'PropA',和localStorage1中'PropA'的双向同步
  @LocalStorageLink('PropA') PropA: string = 'Hello World';
  @State count: number = 0;
  private controller: NodeController = new MyNodeController(this.count, localStorage2);

  build() {
    Row() {
      Column() {
        Text(this.PropA)
          .fontSize(50)
          .fontWeight(FontWeight.Bold)
        // 使用LocalStorage 实例localStorage2
        Child({ count: this.count }, localStorage2)
        NodeContainer(this.controller)
      }
      .width('100%')
    }
    .height('100%')
  }
}

interface Params {
  count: number;
  localStorage: LocalStorage;
}

@Builder
function CreateChild(params: Params) {
  //构造过程中传递localStorage
  Child({ count: params.count }, params.localStorage)
}

class MyNodeController extends NodeController {
  private count?: number;
  private localStorage ?: LocalStorage;

  constructor(count: number, localStorage: LocalStorage) {
    super();
    this.count = count;
    this.localStorage = localStorage;
  }

  makeNode(uiContext: UIContext): FrameNode | null {
    let builderNode = new BuilderNode<[Params]>(uiContext);
    //构造过程中传递localStorage
    builderNode.build(wrapBuilder(CreateChild), { count: this.count, localStorage: this.localStorage });
    return builderNode.getFrameNode();
  }
}

@Component
struct Child {
  @Prop count: number;
  //  'Hello World',和localStorage2中'PropB'的双向同步,如果localStorage2中没有'PropB',则使用默认值'Hello World'
  @LocalStorageLink('PropB') PropB: string = 'Hello World';

  build() {
    Text(this.PropB)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
  }
}

BuilderNode结合ArkWeb组件实现预渲染页面

预渲染适用于Web页面启动与跳转等场景。通过结合BuilderNode,可以将ArkWeb组件提前进行离线预渲染,组件不会即时挂载至页面,而是在需要时通过NodeController动态挂载与显示。此举能够提高页面切换的流畅度及用户体验。

说明

访问在线网页时需添加网络权限:ohos.permission.INTERNET,具体申请方式请参考声明权限

  1. 创建载体Ability,并创建Web组件。

    // 载体Ability
    // EntryAbility.ets
    import { createNWeb } from "../pages/common";
    import { UIAbility } from '@kit.AbilityKit';
    import { window } from '@kit.ArkUI';
    
    export default class EntryAbility extends UIAbility {
      onWindowStageCreate(windowStage: window.WindowStage): void {
        windowStage.loadContent('pages/Index', (err, data) => {
          // 创建ArkWeb动态组件(需传入UIContext),loadContent之后的任意时机均可创建。
          createNWeb("https://www.example.com", windowStage.getMainWindowSync().getUIContext());
          if (err.code) {
            return;
          }
        });
      }
    }
    
  2. 创建NodeContainer和对应的NodeController,渲染后台Web组件。

    // 创建NodeController。
    // common.ets
    import { UIContext } from '@kit.ArkUI';
    import { webview } from '@kit.ArkWeb';
    import { NodeController, BuilderNode, Size, FrameNode }  from '@kit.ArkUI';
    // @Builder中为动态组件的具体组件内容。
    // Data为入参封装类。
    class Data{
      url: string = 'https://www.example.com';
      controller: WebviewController = new webview.WebviewController();
    }
    // 通过布尔变量shouldInactive控制网页在后台完成预渲染后停止渲染。
    let shouldInactive: boolean = true;
    @Builder
    function WebBuilder(data:Data) {
      Column() {
        Web({ src: data.url, controller: data.controller })
          .onPageBegin(() => {
            // 调用onActive,开启渲染。
            data.controller.onActive();
          })
          .onFirstMeaningfulPaint(() =>{
            if (!shouldInactive) {
              return;
            }
            // 在预渲染完成时触发,停止渲染。
            data.controller.onInactive();
            shouldInactive = false;
          })
          .width("100%")
          .height("100%")
      }
    }
    let wrap = wrapBuilder<Data[]>(WebBuilder);
    // 用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。
    export class myNodeController extends NodeController {
      private rootnode: BuilderNode<Data[]> | null = null;
      // 必须要重写的方法,用于构建节点数、返回节点挂载在对应NodeContainer中。
      // 在对应NodeContainer创建的时候调用、或者通过rebuild方法调用刷新。
      makeNode(uiContext: UIContext): FrameNode | null {
        console.info(" uicontext is undifined : "+ (uiContext === undefined));
        if (this.rootnode != null) {
          // 返回FrameNode节点。
          return this.rootnode.getFrameNode();
        }
        // 返回null控制动态组件脱离绑定节点。
        return null;
      }
      // 当布局大小发生变化时进行回调。
      aboutToResize(size: Size) {
        console.info("aboutToResize width : " + size.width  +  " height : " + size.height );
      }
      // 当controller对应的NodeContainer在Appear的时候进行回调。
      aboutToAppear() {
        console.info("aboutToAppear");
        // 切换到前台后,不需要停止渲染。
        shouldInactive = false;
      }
      // 当controller对应的NodeContainer在Disappear的时候进行回调。
      aboutToDisappear() {
        console.info("aboutToDisappear");
      }
      // 此函数为自定义函数,可作为初始化函数使用。
      // 通过UIContext初始化BuilderNode,再通过BuilderNode中的build接口初始化@Builder中的内容。
      initWeb(url:string, uiContext:UIContext, control:WebviewController) {
        if(this.rootnode != null){
          return;
        }
        // 创建节点,需要uiContext。
        this.rootnode = new BuilderNode(uiContext);
        // 创建动态Web组件。
        this.rootnode.build(wrap, { url:url, controller:control });
      }
    }
    // 创建Map保存所需要的NodeController。
    let NodeMap:Map<string, myNodeController | undefined> = new Map();
    // 创建Map保存所需要的WebViewController。
    let controllerMap:Map<string, WebviewController | undefined> = new Map();
    // 初始化需要UIContext 需在Ability获取。
    export const createNWeb = (url: string, uiContext: UIContext) => {
      // 创建NodeController。
      let baseNode = new myNodeController();
      let controller = new webview.WebviewController() ;
      // 初始化自定义Web组件。
      baseNode.initWeb(url, uiContext, controller);
      controllerMap.set(url, controller);
      NodeMap.set(url, baseNode);
    }
    // 自定义获取NodeController接口。
    export const getNWeb = (url : string) : myNodeController | undefined => {
      return NodeMap.get(url);
    }
    
  3. 通过NodeContainer使用已经预渲染的页面。

    // 使用NodeController的Page页。
    // Index.ets
    import { createNWeb, getNWeb } from "./common";
      
    @Entry
    @Component
    struct Index {
      build() {
        Row() {
          Column() {
            // NodeContainer用于与NodeController节点绑定,rebuild会触发makeNode。
            // Page页通过NodeContainer接口绑定NodeController,实现动态组件页面显示。
            NodeContainer(getNWeb("https://www.example.com"))
              .height("90%")
              .width("100%")
          }
          .width('100%')
        }
        .height('100%')
      }
    }