ArkUI-状态管理最佳实践
- 概述
- 合理选择装饰器
- 使用监听和订阅精准控制组件刷新
- @Watch装饰器监听数据源
- 使用自定义事件发布订阅
概述
在声明式UI编程范式中,UI是应用程序状态的函数,应用程序状态的修改会更新响应的UI界面。ArkUI采用了MVVM模式。
ArkUI提供了一系列装饰器实现ViewModel的能力,当自定义组件内变量被装饰器装饰时变为状态变量,状态变量的改变会触发UI渲染刷新。
在ArkUI的开发过程中,如果没有选择合适的装饰器或合理的状态控制更新范围,可能会导致以下问题:
- 状态和UI的不一致,如同一状态的界面元素展示的UI不同,或UI界面展示的不是最新的状态。
- 非必要的UI视图刷新,如只修改局部组件状态时导致组件所在页面的整体刷新。
合理选择装饰器
状态变量的管理有一定的开销,应在合理场景使用,我们应从以下几个方面尽可能的优化状态变量的使用:
- 避免不必要的状态变量的使用
- 删除冗余的状态变量标记
- 使用临时变量替换状态变量,状态变量的变化会引起UI刷新,当我们需要对状态变量进行连续计算等操作时,应使用临时变量进行替换,计算完成后再修改状态变量。
- 最小化共享范围,组件内独享使用@State,组件间共享,ArkUI提供了了
@State+@Prop
、@State+@Link
、@State+@Observed+@ObjectLink
、@Provide+@Consume
、AppStorage
、LocalStorage
六种装饰器组合以解决不同范围内的组件间状态共享。按照共享范围能力从小到大,各装饰器组合的共享范围能力和生命周期如下:- @State+@Prop、@State+@Link、@State+@Observed+@ObjectLink:三者的共享范围为从@State所在的组件开始,到@Prop/@Link/ObjectLink所在组件的整条路径,路径上所有的中间组件通过@Prop/@Link/@ObjectLink都可以共享同一个状态。@State修饰的状态和其所属的自定义组件共享生命周期,在组件内定义时创建,组件销毁时被回收。@Link装饰的变量和其所属的自定义组件共享生命周期。@ObjectLink装饰的变量和其所属的自定义组件共享生命周期。
- @Provide+@Consume:状态共享范围是以@Provide所在组件为祖先节点的整棵子树,子树上的任意后代组件通过@Consume都可以共享同一个状态。@Provide修饰的变量与其所属的组件绑定,在组件内定义时被创建,在组件销毁时被回收。
- LocalStorage:共享范围为UIAbility内以页面为单位的不同组件树间的共享。存储在LocalStorage中的状态的生命周期与LocalStorage绑定。LocalStorage的声明周期由应用程序决定,当应用释放最后一个指向LocalStorage的引用时,LocalStorage被垃圾回收。
- AppStorage:共享范围是应用全局。AppStorage与应用的进程绑定,由UI框架在应用程序启动时创建,当应用进程终止,AppStorage被回收。存储在AppStorage中的状态的生命周期与LocalStorage绑定。
- 减少不必要的层层传递
- 按照状态复杂度选择装饰器
- @State+@Prop组合方案:
- @Prop装饰器支持接收Object、class、string、number、boolean、enum类型,以及这些类型的数组。
- @Prop装饰的变量是对父组件传入状态值的深拷贝,当@Prop装饰器装饰的变量为复杂Object、class或其类型数组时,会增加状态创建时间以及占用大量内存。
- @Prop装饰的变量和父组件是单向绑定的关系。当父组件数据源发生变化时,接收该数据源的@Prop所在组件的实例会重新渲染。 当该组件内被@Prop装饰的变量被修改时,父组件数据源不会变化,父组件实例也不会重新渲染。
- @State+@Link组合方案:
- @Link装饰器支持接收Object、class、string、number、boolean、enum类型,以及这些类型的数组。
- @Link装饰器修饰的变量是对父组件传入状态的引用的拷贝,两者指向同一个地址。当状态是简单数据类型或简单Object类型时,@Link和@Prop在状态创建时间和内存的占用方面区别不大。当状态为复杂的Object、class或其类型数组时,@Link相较@Prop能明显减少状态创建时间和内存的占用。
- @Link装饰器的变量和父组件是双向绑定的关系。当父组件数据源发生变化时,接收该数据源的@Link所在组件的实例会重新渲染。 当该组件内被@Link装饰的变量被修改时,父组件数据源会同步修改,父组件实例也会重新渲染。
- @State+@Observed+@ObjectLink组合方案:
- @ObjectLink只支持接收被@Observed装饰的class实例及继承Date或者Array的class实例。
- @ObjectLink装饰的变量是只读的,不支持对状态重新赋值。
- @ObjectLink必须配合@Observed使用,它的设计是为了解决对嵌套类对象属性变化的监听,如需要观察对象数组中单个数据项的属性值变化,或嵌套对象的对象类型属性的子属性变化。
- @State+@Prop组合方案:
结合三个方案的特性,在选择时有如下建议:
- 需要观察嵌套类对象的深层属性变化的场景,选择@State+@Observed+@ObjectLink。
- 状态是复杂对象、类或其类型数组的场景,选择@State+@Link。
- 状态是简单数据类型时,使用@State+@Link和@State+@Prop均可。在功能层面上,依据@Prop单向绑定的特性,@State+@Prop适合用于非实时修改的场景,如编辑电话薄联系人信息时,展示编辑界面的子组件信息的修改要求不实时同步回父组件,需要等到编辑完成后点击“确认”按钮时才会以事件驱动的方式修改父组件的状态。依据@Link双向绑定的特性,@State+@Link适合用于实时修改的场景,如组件嵌套时的滚动条同步。
使用监听和订阅精准控制组件刷新
多个组件依赖对象中的不同属性时,直接关联该对象会出现改变任一属性所有组件都刷新的现象,可以通过将类中的属性拆分组合成新类的方式精准控制组件刷新。
@Watch装饰器监听数据源
在组件中使用@Watch装饰器监听数据源,当数据变化时执行业务逻辑,确保只有满足条件的组件进行刷新。
@Entry
@Component
struct UseWatchListener {
@State currentIndex: number = 0; // 当前选中的列表项下标
private listData: string[] = [];
aboutToAppear(): void {
for (let i = 0; i < 10; i++) {
this.listData.push(`组件 ${i}`);
}
}
build() {
Row() {
Column() {
List() {
ForEach(this.listData, (item: string, index: number) => {
ListItem() {
ListItemComponent({ item: item, index: index, currentIndex: this.currentIndex })
}
})
}
.height('100%')
.width('100%')
.alignListItem(ListItemAlign.Center)
}
.width('100%')
}
.height('100%')
}
}
@Component
struct ListItemComponent {
@Prop item: string;
@Prop index: number; // 列表项的下标
@Link @Watch('onCurrentIndexUpdate') currentIndex: number;
@State color: Color = Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue;
isRender(): number {
console.info(`ListItemComponent ${this.index} Text is rendered`);
return 50;
}
onCurrentIndexUpdate() {
// 根据当前列表项下标index与currentIndex的差值来动态修改color的值
this.color = Math.abs(this.index - this.currentIndex) <= 1 ? Color.Red : Color.Blue;
}
build() {
Column() {
Text(this.item)
.fontSize(this.isRender())
.fontColor(this.color)
.onClick(() => {
this.currentIndex = this.index;
})
}
}
}
上述代码中,ListItemComponent组件中的状态变量currentIndex使用@Watch装饰,Text组件直接关联新的状态变量color。当currentIndex发生变化时,会触发onCurrentIndexUpdate方法,在其中将表达式的运算结果赋值给状态变量color。只有color的值发生变化时,Text组件才会重新渲染,运行效果图如下:
被依赖的数据源仅在父子或兄弟关系的组件中传递时,可以参考上述示例,使用@State/@Link/@Watch装饰器进行状态管理,实现组件的精准刷新。
当组件关系层级较多但都归属于同一个确定的组件树时,推荐使用@Provide/@Consume传递数据,使用@Watch装饰器监听数据变化,在监听回调中执行业务逻辑。
使用自定义事件发布订阅
当组件关系复杂或跨越层级过多时,推荐使用EventHub或者Emitter自定义事件发布订阅的方式。当数据源改变时发布事件,依赖该数据源的组件通过订阅事件来获取数据源的改变,完成业务逻辑的处理,从而实现组件的精准刷新。