概述
- 什么是动态创建组件
所谓的动态创建组件就是在运行时根据实际需要渲染相应的组件, 可以 动态创建组件(动态添加组件)、动态卸载组件(动态删除组件)等操作
- 为何需要使用动态化的方式创建组件.
动态创建组件指在非build生命周期中进行组件创建,即在build生命周期前提前创建组件。通过动态创建组件,开发者不但可以节省组件创建的时间,提升用户体验,还可以将独立的逻辑进行封装,开发者能更加灵活地管理组件,有助于应用模块化开发
比较在build中创建组件和通过动态创建组件的区别
组件在build环节中被创建,开发者无法在其他生命周期阶段进行组件的创建,从而引起页面加载慢等问题
动态操作支持组件的预创建。组件预创建可以满足开发者在非build生命周期中进行组件创建,创建后的组件可以进行属性设置、布局计算等操作。之后在页面加载时进行使用,可以极大提升页面响应速度
图片来源于官网: 声明式UI中实现组件动态创建-应用框架开发-功能开发 | 华为开发者联盟 (huawei.com)
利用组件预创建机制,可以利用动画执行过程空闲时间进行组件预创建和属性设置。在动画结束后,再进行属性和布局的更新,节省了组件创建的时间,从而加快了页面渲染
实现
创建动态组件
实现这个功能的大致步骤如下
- 通过
@Builder的方式创建用于渲染的组件 - 提供一个类, 该类需要继承NodeController, 同时实现父类中的MakeNode方法, 该方法用于提供具体渲染的节点
- 在调用该组件的地方创建对应的实例, 然后关联到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该类提供的一些能力并不多
更新动态组件
动态将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中的组件数据, 不能直接修改, 我们需要
- 在builder中调用自定义组件
- 在该自定义组件中使用@Prop接受变量, 必须为Prop
- 在NodeController中提供一个调用BuilderNode的build方法的接口
- 父组件的数据发生变化时, 调用该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)