鸿蒙6.0应用开发——状态管理最佳实践
在声明式UI编程范式中,UI是应用程序状态的函数,应用程序状态的修改会更新相应的UI界面。ArkUI采用了[MVVM](https://developer.huawei.com/consumer/cn/doc/harmonyos-guides/arkts-mvvm)模式,其中ViewModel将数据与视图绑定在一起,更新数据的时候直接更新视图。
鸿蒙6.0应用开发——状态管理最佳实践
概述
在声明式UI编程范式中,UI是应用程序状态的函数,应用程序状态的修改会更新相应的UI界面。ArkUI采用了MVVM模式,其中ViewModel将数据与视图绑定在一起,更新数据的时候直接更新视图。如下图所示:
图1 ArkUI的MVVM模式

ArkUI提供了一系列装饰器实现ViewModel的能力,如@Prop装饰器:父子单向同步、@Link装饰器:父子双向同步、@Provide装饰器和@Consume装饰器:与后代组件双向同步、LocalStorage:页面级UI状态存储等。当自定义组件内变量被装饰器装饰时变为状态变量,状态变量的改变会引起UI的渲染刷新。
在ArkUI的开发过程中,如果没有选择合适的装饰器或合理的控制状态更新范围,可能会导致以下问题:
\1. 状态和UI的不一致,如同一状态的界面元素展示的UI不同,或UI界面展示的不是最新的状态。
\2. 非必要的UI视图刷新,如只修改局部组件状态时导致组件所在页面的整体刷新。
当用户与界面产生交互行为时,状态的修改是通过事件驱动处理的。事件的处理可以在应用的任何地方,如果没有进行适当的逻辑处理管理也会导致代码冗余和不利于维护。
本文旨在从装饰器的选择、使用以及状态的逻辑处理管理方面解决以上问题,以实现更好的状态管理。
合理选择装饰器
避免不必要的状态变量的使用
删除冗余的状态变量标记
状态变量的管理有一定的开销,应在合理场景使用,普通的变量用状态变量标记可能会导致性能劣化。
反例1
@Observed
class Translate {
translateX: number = 20;
}
@Component
struct MyComponent {
@State translateObj: Translate = new Translate(); // The variable translateObj is not associated with any UI component and should not be defined as a state variable
@State buttonMsg: string = 'I am button'; // The variable buttonMsg is not associated with any UI component and should not be defined as a state variable
build() {
}
}
以上示例中变量translateObj,buttonMsg没有关联任何UI组件,没有关联任何UI组件的状态变量不应该定义为状态变量,否则读写状态变量都会影响性能。
反例2
@Observed
class Translate {
translateX: number = 20;
}
@Component
struct MyComponent {
@State translateObj: Translate = new Translate();
@State buttonMsg: string = 'I am button';
build() {
Column() {
Button(this.buttonMsg) // Here we just read the value of the variable buttonMsg, without any write operation.
}
}
}
以上示例中变量buttonMsg仅有读取操作,没有修改操作,未修改过的状态变量不应定义为状态变量,否则读状态变量会影响性能。
正例
@Observed
class Translate {
translateX: number = 20;
}
@Component
struct UnnecessaryState1 {
@State translateObj: Translate = new Translate(); // If there are both read and write operations and a Button component is associated with it, it is recommended to use state variables.
buttonMsg = 'I am button'; // Only read the value of the variable buttonMsg, without any write operations, just use the general variables directly
build() {
Column() {
Button(this.buttonMsg)
.onClick(() => {
this.getUIContext().animateTo({
duration: 50
}, () => {
this.translateObj.translateX = (this.translateObj.translateX + 50) % 150; // Reassign value to variable translateObj when clicked.
})
})
}
.translate({
x: this.translateObj.translateX // Retrieve the value in translateObj.
})
}
}
代码逻辑走读:
- 定义了一个
Translate类,并使用@Observed装饰器使其成为可观察对象,包含一个数值属性translateX,初始值为20。 - 定义了一个组件
UnnecessaryState1,内部包含一个状态变量translateObj,它是Translate类的实例。 - 在组件的
build方法中,创建了一个Column布局,内部包含一个Button组件。 Button组件的onClick事件处理函数中,通过animateTo方法定义了一个动画,动画的执行时间为50毫秒。- 在动画的回调函数中,更新
translateObj.translateX的值,使其在每次点击时增加50,并取模150以确保值在合理范围内循环。 Column布局的translate属性使用translateObj.translateX的值来控制其在X轴的平移。
在代码中,buttonMsg变量因仅用于读取操作而被定义为普通成员变量,而translateObj变量则因需要根据用户事件改变其x值以驱动动画效果,故被定义为状态变量,并实时更新UI以显示动画。
建议使用临时变量替换状态变量
状态变量发生变化时,ArkUI会查询依赖该状态变量的组件并执行依赖该状态变量的组件的更新方法,完成组件渲染的行为。通过使用临时变量的计算代替直接操作状态变量,可以使ArkUI仅在最后一次状态变量变更时查询并渲染组件,减少不必要的行为,从而提高应用性能。状态变量行为可参考@State装饰器:组件内状态。
反例
@Component
struct Index {
@State message: string = '';
// Define methods for changing state variables (multiple modifications of state variables)
appendMsg(newMsg: string) {
this.message += newMsg;
this.message += ';';
this.message += '<br/>';
}
build() {
Column() {
Button('Click Print Log')
.onClick(() => {
this.appendMsg('Operational state variables'); // Calling encapsulated methods for changing state variables
})
.width('90%')
.backgroundColor(Color.Blue)
.fontColor(Color.White)
.margin({ top: 10})
}
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
.margin({ top: 15 })
}
}
代码逻辑走读:
- 组件定义与状态初始化:
- 使用
@Component装饰器定义了一个名为Index的组件。 - 使用
@State装饰器定义了一个状态变量message,初始值为空字符串。
- 使用
- 方法定义:
- 定义了一个名为
appendMsg的方法,该方法接受一个字符串参数newMsg。 - 在
appendMsg方法中,将newMsg追加到message变量中,并添加分隔符和换行符。
- 定义了一个名为
- 构建方法:
- 在
build方法中,创建了一个Column布局容器。 - 在
Column中,添加了一个按钮,按钮文本为’Click Print Log’。 - 按钮的点击事件绑定了一个匿名函数,该函数调用
appendMsg方法,传入字符串’Operational state variables’。 - 按钮设置了宽度、背景色、字体颜色和上边距。
Column设置了内容对齐方式、水平对齐方式和上边距。
- 在
正例
@Entry
@Component
struct UnnecessaryState2 {
@State message: string = '';
// Define methods for changing state variables (intermediate variables are manipulated during method execution, state variables are modified only once)
appendMsg(newMsg: string) {
let message = this.message;
message += newMsg;
message += ';';
message += '<br/>';
this.message = message;
}
build() {
Column() {
Button('Click Print Log')
.onClick(() => {
this.appendMsg('Manipulating Temporary Variables'); // Calling encapsulated methods for changing state variables
})
.width('90%')
.backgroundColor(Color.Blue)
.fontColor(Color.White)
.margin({ top: 10 })
}
.justifyContent(FlexAlign.Start)
.alignItems(HorizontalAlign.Center)
.margin({ top: 15 })
}
}
代码逻辑走读:
-
组件定义:使用
@Entry和@Component装饰器定义了一个名为UnnecessaryState2的组件。 -
状态变量声明:在组件中声明了一个状态变量
message,初始值为空字符串。 -
方法定义:定义了一个名为appendMsg的方法,该方法接收一个字符串参数newMsg,用于修改message
状态变量的值。
- 在方法内部,首先将当前的
message值存储在局部变量message中。 - 然后将
newMsg追加到message变量中,并添加分隔符和换行符。 - 最后,将更新后的
message赋值回状态变量this.message。
- 在方法内部,首先将当前的
-
UI构建
:定义了组件的UI结构,使用Column布局包含一个按钮。
- 按钮的文本为"Click Print Log",点击时会调用
appendMsg方法。 - 按钮具有特定的样式,如宽度、背景色、字体色和边距。
Column布局设置了内容的对齐方式和外边距。
- 按钮的文本为"Click Print Log",点击时会调用
最小化状态共享范围
在没有强烈的业务需求下,尽可能按照状态需要共享的最小范围选择合适的装饰器。应用开发过程中,按照组件颗粒度,状态一般分为组件内独享的状态和组件间需要共享的状态。
组件内独享的状态
组件内独享的状态的生命周期和组件同步,状态的定义和更新都在组件内,组件销毁,状态也随即消失。常见于界面UI元素数据,比如当前按钮是否可用、文字是否高亮等。组件内独享的状态使用@State装饰器,被@State装饰器修饰后状态的修改只会触发当前组件实例的重新渲染。如下图主题列表上单个主题组件内使用@State修饰主题是否被选中的变量,当在界面点击主题时在组件内直接修改状态值。此时,只有当前主题的组件实例会重新渲染,其他主题组件不会重新渲染。
图2 HMOS世界App主题选择交互图
组件间需要共享的状态
组件间需要共享的状态,按照共享范围从小到大依次有三种场景:父子组件间共享状态,不同子树上组件间共享状态和不同组件树间共享状态。
-
父子组件间共享状态:如下图,”父组件”和其子组件”子组件A”、”子组件B”共享状态loading。
图3 父子组件间共享状态场景

-
不同子树上组件间共享状态:如下图,祖先组件的左子树上”孙子组件AAA”和右子树上”孙子组件BAA”共享状态loading。
图4 不同子树上组件间状态共享场景

-
不同组件树间共享状态:如下图,组件树A内”子组件AA”和组件树B内”孙子组件BAA”共享状态loading。
图5 不同组件树间共享状态的场景

对于上述三种场景,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被回收。
按照软件开发原则,应优先选择共享范围能力小的装饰器方案,减少不必要的参数层层传递,降低不同模块间的数据耦合,便于状态及时回收。建议选择装饰器的优先级为:@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink > @Provide+@Consume > LocalStorage > AppStorage。
减少不必要的参数层层传递
当按照上述优先级选择装饰器时,由于@State+@Prop、@State+@Link、@State+@Observed+@ObjectLink三种方案的实现方式是逐级向下传递状态,当共享状态的组件间层级相差较大时,会出现状态层层传递的现象。对于状态传递过程中途经的全部组件,都需要增加入参接收该状态再将状态传递给子组件。对于没有使用该状态的中间组件而言,这是“额外的消耗”,不利于代码的维护和拓展。尤其是当业务体系庞大时,需求变更容易出现“牵一发而动全身”的问题。
以“HMOS世界App”中路由状态为例,其“探索”Tab和“我的”Tab界面组件如下:
图6 HMOS世界App界面组件示意图

- “MainPage”是主页面,该页面有2个子组件“MineView”和“DiscoverView”。
- “MineView”是“我的”Tab对应的内容视图组件,“CollectedResourceView”是该组件内展示收藏列表的视图组件,“ResourceListView”是“CollectedResourceView”的子组件。
- “DiscoverView”是“探索”Tab对应的内容视图组件,“TechArticlesView”是该组件内展示文章列表的视图组件,“ArticleCardView”是列表上单个卡片视图组件,“ActionButtonView”是卡片上交互视图组件。
项目中使用Navigation组件管理路由,定义“appNavigationStack”变量表示应用当前的路由信息。现“DiscoverView”组件和“ResourceListView”组件需要共享路由信息状态。按照@State+@Prop层层传递的方案,当前各组件的设计如下:
图7 @State+@Prop当前组件设计图

可以看到,为了实现“ResourceListView”组件和“DiscoverView”组件共享状态,将状态定义在两者的最近公共祖先“MainPage”组件上。对公共祖先到两个需要共享路由状态的组件路径上的所有组件使用@Prop装饰器接收“appNavigationStack”参数,层层传递,直到两个需要共享状态的组件。
若此时产品需要新增功能,该功能要求在“DiscoverView”组件的后代“ActionButtonView”组件上新增对路由信息的判断逻辑。此时开发者需修改上述各个组件设计如下图所示:
图8 @State+@Prop新增功能后组件设计图
可以看到,新功能的逻辑原本只是在“ActionButtonView”这一个组件中使用,开发者却需要修改从“DiscoverView”组件到“ActionButtonView”组件路径上3个组件的结构。若当业务后续再次变更为无需使用该状态时,也同样需要修改多个组件。这显然不是很好的实现方案。
此时使用@Provide+@Consume方案更为合理。同样是“ResourceListView”组件和“DiscoverView”组件共享状态,此方案各组件设计如下:
图9 使用@Provide+@Consume方式当前各组件设计
通过在最顶部组件“MainPage”中注入key值为“appNavigationStack”的路由信息状态,其后代组件均可以通过@Consume装饰器获取该状态值。当业务变动需要“DiscoverView”的后代“ActionButtonView”组件也共享路由信息时,此方案只需在组件“ActionButtonView”上使用@Consume装饰器直接获取路由信息状态,而无需修改其他组件。此时各组件设计如下:
图10 使用@Provide+@Consume方式业务变动后各组件设计
因此当共享状态的组件间跨层级较深时,或共享的信息对于整个组件树是“全局”的存在时,选择@Provide+@Consume的装饰器组合代替层层传递的方式,能够提升代码的可维护性和可拓展性。
按照状态复杂度选择装饰器
对于上述具有相同优先级的装饰器选择方案@State+@Prop、@State+@Link和@State+@Observed+@ObjectLink。在选择方案时,需要结合具体的业务场景和状态数据结构的复杂度。这三种不同的装饰器组合方案在内存消耗、性能消耗和对数据类型的支持能力都不相同,如下:
图11 @State+@Prop、@State+@Link和@State+@Observed+@ObjectLink装饰器方案区别

- @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+@Observed+@ObjectLink。
- 状态是复杂对象、类或其类型数组的场景,选择@State+@Link。
- 状态是简单数据类型时,使用@State+@Link和@State+@Prop均可。在功能层面上,依据@Prop单向绑定的特性,@State+@Prop适合用于非实时修改的场景,如编辑电话簿联系人信息时,展示编辑界面的子组件信息的修改要求不实时同步回父组件,需要等到编辑完成后点击“确认”按钮时才会以事件驱动的方式修改父组件的状态。依据@Link双向绑定的特性,@State+@Link适合用于实时修改的场景,如组件嵌套时的滚动条同步。
总结
在实际开发中,合理选择装饰器主要包含以下三步:
1.首先根据状态需要共享的范围大小,尽量选择共享能力小的装饰器方案,优先级依次为@State+@Prop、@State+@Link或@State+@Observed+@ObjectLink > @Provide+@Consume > LocalStorage > AppStorage。
2.当共享的状态的组件间层级相差较大时,为避免较差的代码可扩展性和可维护性,@Provide+@Consume的方案要优于层层传递的共享方案。
3.对于具有相同优先级的@State+@Prop、@State+@Link或@State+@Observed+@ObjectLink 三个方案,应结合状态的复杂程度和装饰器各自的特性选择。
实际开发中,应根据业务需求衡量优先级选择合适的装饰器,整体可参考如下建议:
- @State+@Prop:适合状态结构简单,且共享状态的组件间层级相差不大的场景。或功能上要求子组件不实时同步修改给父组件的场景。
- @State+@Link:适合状态结构复杂,且共享状态的组件间层级相差不大的场景。或功能上要求子组件对状态的修改实时同步给父组件的场景。
- @State+@Observed+@ObjectLink:适合需要观察嵌套类对象的子属性变化的场景或对象数组的数据项属性变化的场景,如监听列表卡片上某个属性的变化。
- @Provide+@Consume:适合用于对于整个组件树而言“全局”的状态,且该状态改动不频繁的状态共享场景,如共享界面的路由信息。
- AppStorage:适合对于整个应用而言“全局”的变量或应用的主线程内多个UIAbility实例间的状态共享,如用户信息。
间层级相差不大的场景。或功能上要求子组件对状态的修改实时同步给父组件的场景。 - @State+@Observed+@ObjectLink:适合需要观察嵌套类对象的子属性变化的场景或对象数组的数据项属性变化的场景,如监听列表卡片上某个属性的变化。
- @Provide+@Consume:适合用于对于整个组件树而言“全局”的状态,且该状态改动不频繁的状态共享场景,如共享界面的路由信息。
- AppStorage:适合对于整个应用而言“全局”的变量或应用的主线程内多个UIAbility实例间的状态共享,如用户信息。
- LocalStorage:适合对于单个Ability而言“全局”的变量,主要用于不同页面间的状态共享场景。
更多推荐



所有评论(0)