【HarmonyOS实战开发】鸿蒙中动态创建组件

213 篇文章 0 订阅
213 篇文章 0 订阅

概述

  • 什么是动态创建组件

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

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

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

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

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

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

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

实现

创建动态组件

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

  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的点击事件就不会

写在最后

●如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
●点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
●关注小编,同时可以期待后续文章ing🚀,不定期分享原创知识。
●更多鸿蒙最新技术知识点,请移步前往小编:https://gitee.com/

  • 17
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值