鸿蒙中动态创建组件

389 阅读7分钟

概述

  • 什么是动态创建组件

所谓的动态创建组件就是在运行时根据实际需要渲染相应的组件, 可以 动态创建组件(动态添加组件)、动态卸载组件(动态删除组件)等操作

  • 为何需要使用动态化的方式创建组件.

动态创建组件指在非build生命周期中进行组件创建,即在build生命周期前提前创建组件。通过动态创建组件,开发者不但可以节省组件创建的时间,提升用户体验,还可以将独立的逻辑进行封装,开发者能更加灵活地管理组件,有助于应用模块化开发

比较在build中创建组件和通过动态创建组件的区别

组件在build环节中被创建,开发者无法在其他生命周期阶段进行组件的创建,从而引起页面加载慢等问题

动态操作支持组件的预创建。组件预创建可以满足开发者在非build生命周期中进行组件创建,创建后的组件可以进行属性设置、布局计算等操作。之后在页面加载时进行使用,可以极大提升页面响应速度

图片来源于官网: 声明式UI中实现组件动态创建-应用框架开发-功能开发 | 华为开发者联盟 (huawei.com)

image.png

利用组件预创建机制,可以利用动画执行过程空闲时间进行组件预创建和属性设置。在动画结束后,再进行属性和布局的更新,节省了组件创建的时间,从而加快了页面渲染

实现

创建动态组件

实现这个功能的大致步骤如下

  1. 通过@Builder的方式创建用于渲染的组件
  2. 提供一个类, 该类需要继承NodeController, 同时实现父类中的MakeNode方法, 该方法用于提供具体渲染的节点
  3. 在调用该组件的地方创建对应的实例, 然后关联到container中.
  • CustomText.ets 该文件提供组件, 以及NodeController子类
// 用于渲染的节点, 为了更加的通用, 可以传递参数
@Builder
function buildText(params: string) {
  Column() {
    Text(params)
      .fontSize(50)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
      .onClick(() => {
        promptAction.showToast({ message: '我触发了' })
      })
  }
}

// 后续container中需要的实例对象
export class TextNodeController extends NodeController {
  // private text: Params = new Params('')
  private text: string = ''

  constructor(param: string) {
    // 因为有了继承, 一定要调用super()
    super()
    this.text = param
  }

  // 在挂载时触发的逻辑, 需要提供一个具体的node
  makeNode(uiContext: UIContext): FrameNode | null {
    // 通过一个BuilderNode对我们定义的全局builder函数进行封装
    const textNode = new BuilderNode<[string]>(uiContext)
    textNode.build(wrapBuilder<[string]>(buildText), this.text)
    return textNode.getFrameNode()
  }
}

方法build()需要传入两个参数,第一个参数为通过wrapBuilder()封装的全局@Builder方法。第二个参数为对应的@Builder方法所需的参数对象。若@Builder方法不带参数或者存在默认参数,则build()的第二个参数可以不设置

  • Index.ets 在首页中进行调用
@Entry
@Component
struct Index {
  @State msg: string = "别来无恙!"
  private textNodeController: TextNodeController = new TextNodeController(this.msg)

  build() {
    RelativeContainer() {
      // NodeContainer只能用一些通用的属性
      NodeContainer(this.textNodeController)
        .id('nodeContainer')
        .alignRules({
          center: { anchor: '__container__', align: VerticalAlign.Center },
          middle: { anchor: '__container__', align: HorizontalAlign.Center }
        })
    }
    .height('100%')
    .width('100%')
  }
}

删除动态组件

直接对NodeContainer容器进行if渲染即可.

@Entry
@Component
struct Index {
  @State showText: boolean = true
    ...
    
  build() {
    RelativeContainer() {
      if (this.showText) {
        // NodeContainer只能用一些通用的属性
        NodeContainer(this.textNodeController)
          ...
      }
​
      Button(this.showText ? '隐藏文字' : '显示文字')
        .id('button01')
        .onClick(() => {
          this.showText = !this.showText
        })
        .alignRules({
          left: { anchor: 'nodeContainer', align: HorizontalAlign.Start },
          right: { anchor: 'nodeContainer', align: HorizontalAlign.End },
          top: { anchor: 'nodeContainer', align: VerticalAlign.Bottom }
        })
    }
    .height('100%')
    .width('100%')
  }
}

给Controller添加一些钩子函数

export class TextNodeController extends NodeController {
  // ...
  aboutToDisappear(): void {
    promptAction.showToast({ message: '我消失显示了' })
  }
}

注意: 使用Visibility.None来隐藏元素时, 不会触发onDisappear

Controller该类提供的一些能力并不多

image.png

更新动态组件

动态将NodeContainer上的节点替换,依赖于NodeController的makeNode和rebuild方法。rebuild方法会触发makeNode的回调,刷新NodeContainer上显示的节点;makeNode方法返回的为null,则移除NodeContainer下挂载的节点

index.ets页面, 添加一个全局builder, 以及添加一个修改组件的button

@Entry
@Component
struct Index {
  // ...
  private textNodeController: TextNodeController = new TextNodeController(this.msg)

  build() {
    RelativeContainer() {
      if (this.showText) {
        // NodeContainer只能用一些通用的属性
        NodeContainer(this.textNodeController)
          ...
      }
      ...

      Button('替换组件')
        .id('button02')
        .onClick(() => {
          // 构建新的组件对象
          const newBuildNode = new BuilderNode<[string]>(this.getUIContext())
          newBuildNode.build(wrapBuilder<[string]>(NewTextNodeBuilder), '好久不见')
          this.textNodeController.changeNode(newBuildNode)
        })
        .margin({ top: 10 })
        .alignRules({
          left: { anchor: 'button01', align: HorizontalAlign.Start },
          right: { anchor: 'button01', align: HorizontalAlign.End },
          top: { anchor: 'button01', align: VerticalAlign.Bottom }
        })
    }
    .height('100%')
    .width('100%')
  }
}

// 用于渲染的builder
@Builder
function NewTextNodeBuilder(params: string) {
  Text(params)
    .fontSize(50)
    .fontWeight(FontWeight.Lighter)
    .margin({ bottom: 24 })
    .onClick(() => {
      promptAction.showToast({ message: '我触发了' })
    })
}

修改CustomText.ets 文件

export class TextNodeController extends NodeController {
  // private text: Params = new Params('')
  private text: string = ''
  // 置为null方便后续进行更新组件的时候的控制
  private textNode: BuilderNode<[string]> | null = null;

  // ...

  // 在挂载时触发的逻辑, 需要提供一个具体的node
  makeNode(uiContext: UIContext): FrameNode | null {
    // 第一次渲染, 该属性为null
    if (this.textNode === null) {
      this.textNode = new BuilderNode<[string]>(uiContext)
      this.textNode.build(wrapBuilder<[string]>(buildText), this.text)
    }

    // 后续被rebuild调用时, 不会触发上面的if逻辑
    return this.textNode.getFrameNode()
  }

  // 提供动态更新组件的方法
  changeNode(newNode: BuilderNode<[string]>) {
    // rebuild方法来源于父类实现
    // 要求参数一致
    this.textNode = newNode
    this.rebuild()
    promptAction.showToast({ message: '哎呦, 你干嘛' })
  }
}

NodeController生命周期

NodeController用于控制和反馈对应的NodeContainer上的节点的行为,需要与NodeContainer一起使用。下面,对其常用生命周期函数进行说明。

  • makeNode必须要重写的方法,用于构建节点树、返回节点挂载在对应NodeContainer中。在对应NodeContainer创建绑定当前NodeController的时候调用、或者通过rebuild方法调用刷新。
  • aboutToResize当controller对应的NodeContainer在Mesure的时候进行回调,入参为节点的布局大小。
  • aboutToAppear当controller对应的NodeContainer在onAppear的时候进行回调。
  • aboutToDisappear当controller对应的NodeContainer在onDisappear的时候进行回调

响应式数据

现在如果我们希望动态组件中数据也是响应式的, 该如何处理?

我们传递的似乎是一个builder, 那是否可以根据以前给builder传递状态变量的方式, 进行传递 例如我们传递一个对象的方式, 传递变量, 是否可行? 这里是不行的

如果想要更新builder中的组件数据, 不能直接修改, 我们需要

  1. 在builder中调用自定义组件
  2. 在该自定义组件中使用@Prop接受变量, 必须为Prop
  3. 在NodeController中提供一个调用BuilderNode的build方法的接口
  4. 父组件的数据发生变化时, 调用该NodeController暴漏的build方法的接口

新建一个文件 DynamicCompController.ets

// builder中的子组件, 该组件用于数据的响应式
@Component
struct CustomComp {
  // 需要使用Prop进行装饰
  @Prop data: string = ''

  build() {
    Text(this.data)
      .fontSize(50)
      .fontColor(Color.Blue)
      .fontWeight(FontWeight.Bold)
      .margin({ bottom: 36 })
  }
}

@Builder
export function ShowParentTextBuilder(params: Params) {
  Column(){
    Text('具有状态的动态组件builder')
    CustomComp({ data: params.text })
  }
}

重新定义一个类, 该类中提供一个调用BuilderNode的build方法的接口

export class DynamicCompControllerViewModel extends NodeController {
  private builderNode: BuilderNode<[Params]> | null = null
  private data: Params

  constructor(data: Params, builderNode: BuilderNode<[Params]>) {
    super()

    this.data = data
    this.builderNode = builderNode
  }

  makeNode(uiContext: UIContext): FrameNode | null {
    if (this.builderNode === null) {
      return null
    }
    return this.builderNode.getFrameNode()
  }

  // 外部调用的更新的方法
  updateData(newData: Params) {
    // update的数据类型要和builder一致
    this.builderNode?.update(newData)
  }
}

使用

@Entry
@Component
struct Index {
  // 父组件中会修改的数据
  @State @Watch('updateDynamicCompData') inputData: string = '嗨, 别来无恙!'
  
  // 用于Container展示的具体的node实例
  private nBuildNode: BuilderNode<[Params]> = new BuilderNode<[Params]>(this.getUIContext())
  private textNodeController: DynamicCompControllerViewModel =
    new DynamicCompControllerViewModel({ text: this.inputData }, this.nBuildNode)

  aboutToAppear(): void {
    // 将builder和node实例进行关联
    this.nBuildNode.build(wrapBuilder<[Params]>(ShowParentTextBuilder), { text: this.inputData })
  }

  updateDynamicCompData() {
    // 每次数据发生变化时, 更新node的数据
    this.textNodeController.updateData({ text: this.inputData })
  }

  build() {
    Column() {
      TextInput({ text: $$this.inputData })
        .id('textInput')
        .style(TextInputStyle.Default)
        .type(InputType.Normal)
        .borderRadius(6)
        .margin(20)

      NodeContainer(this.textNodeController)
        .id('nodeContainer')
        .onClick(() => {
          promptAction.showToast({ message: '哎呦' })
        })
    }
  }
}

注意事项

NodeContainer组件本身的一些事件是在其包裹的builder函数之后响应的

例如builder函数中定义了点击事件, 那NodeContainer的点击事件就不会响应了

视频讲解链接: 通过百度网盘分享的文件:动态创建组件 链接:pan.baidu.com/s/1mnqc7BfN… 提取码:4h7c

参考

声明式UI中实现组件动态创建-应用框架开发-功能开发 | 华为开发者联盟 (huawei.com)

BuilderNode-arkui-UI界面-ArkTS API-ArkUI(方舟UI框架)-应用框架 | 华为开发者联盟 (huawei.com)