仓颉 ArkUI 状态管理(V1)机制全解:从状态变量到应用级状态存储
仓颉 ArkUI 状态管理(V1)机制全解:从状态变量到应用级状态存储
状态管理是构建动态、可交互声明式 UI 的基石:在仓颉(Cangjie)ArkUI 开发中,UI 是程序状态的运行结果,状态变化驱动渲染刷新。本文以状态管理概述文档为主体,系统讲解状态与 UI 的关系、状态变量的五大基本概念,并全景梳理状态管理 V1 提供的组件级装饰器(@State、@Prop、@Link、@Provide/@Consume、@Observed/@Publish)与应用级状态存储(AppStorage、LocalStorage、PersistentStorage、Environment)以及监听能力 @Watch,帮助开发者建立状态管理的整体认知,为后续深入学习各装饰器细节和落地复杂业务场景打下基础。
为什么需要状态管理:UI 是状态的函数
在静态界面中,组件渲染一次后便不再变化;而希望构建一个动态的、有交互的界面,就需要引入"状态"的概念。典型场景如下:用户与应用程序的交互触发了文本状态变更,状态变更引起 UI 渲染,UI 从"Hello World"变更为"Hello Cangjie"。
在声明式 UI 编程框架中,UI 是程序状态的运行结果:
- View(UI):UI 渲染,指将
build方法内的 UI 描述和@Builder装饰的方法内的 UI 描述映射到界面。 - State:状态,指驱动 UI 更新的数据。用户通过触发组件的事件方法,改变状态数据;状态数据的改变,引起 UI 的重新渲染。
用户构建了一个 UI 模型,应用的运行时状态是参数。当参数改变时,UI 作为返回结果也进行对应的改变。这些运行时状态变化所带来的 UI 重新渲染,在仓颉中统称为状态管理机制。
自定义组件拥有变量,变量必须被装饰器装饰才可以成为状态变量,状态变量的改变会引起 UI 的渲染刷新。如果不使用状态变量,UI 只能在初始化时渲染,后续将不会再刷新。
说明: 当前状态管理的功能仅支持在 UI 主线程使用,不能在子线程、worker、taskpool 中使用。
状态管理的基本概念
理解状态管理机制,首先需要掌握以下核心概念:
- 状态变量:被状态装饰器装饰的变量,状态变量值的改变会引起 UI 的渲染更新。例如
@State var num: Int32 = 1,其中@State是状态装饰器,num是状态变量。 - 常规变量:没有被状态装饰器装饰的变量,通常应用于辅助计算。它的改变永远不会引起 UI 的刷新。
- 数据源/同步源:状态变量的原始来源,可以同步给不同的状态数据,通常指父组件传给子组件的数据。
- 命名参数机制:父组件通过指定参数传递给子组件的状态变量,是父子传递同步参数的主要手段,例如
CompA(aProp: this.aProp)。 - 从父组件初始化:父组件使用命名参数机制,将指定参数传递给子组件。子组件初始化的默认值在有父组件传值的情况下会被覆盖。
- 初始化子组件:父组件中状态变量可以传递给子组件,初始化子组件对应的状态变量。
- 本地初始化:在变量声明的时候赋值,作为变量的默认值,例如
@State count: Int32 = 0。
以下示例完整展示了"从父组件初始化覆盖本地默认值"的机制:
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Component
class MyComponent {
@State var count: Int32 = 0
private var increaseBy: Int32 = 1
func build() {}
}
@Entry
@Component
class Parent {
func build() {
Column() {
// 从父组件初始化,覆盖本地定义的默认值
MyComponent(count: 1, increaseBy: 2 )
}
}
}
在本示例中,@State 装饰的 count 是状态变量,increaseBy 是常规变量(辅助计算用);父组件通过命名参数 count: 1, increaseBy: 2 初始化子组件,其中 1 和 2 即数据源。
状态管理 V1 装饰器总览
开发者可以选择使用状态管理 V1 版本进行应用开发。V1 提供了多种装饰器,通过它们,状态变量不仅可以观察在组件内的改变,还可以在不同组件层级间传递(如父子组件、跨组件层级),也可以观察全局范围内的变化。
从影响范围维度,装饰器可分为两类:
- 管理组件内状态的装饰器:组件级别的状态管理,可以观察同一个组件树上(即同一个页面内)组件内或不同组件层级的变量变化。
- 管理应用级状态的装饰器:应用级别的状态管理,可以观察不同页面、甚至不同 UIAbility 的状态变化,是应用内全局的状态管理。
从数据的传递形式和同步类型维度,装饰器可分为:
- 只读的单向传递;
- 可变更的双向传递。
基于以上两个维度,开发者可以灵活地利用这些能力实现数据和 UI 的联动。下文分别梳理组件级与应用级两大类状态管理能力,各装饰器的详细规则可点击对应链接查看。
管理组件拥有的状态(组件级)
即组件级别的状态管理,包括 @State、@Prop、@Link、@Provide/@Consume、@Observed/@Publish 六个装饰器。
@State:组件内状态
@State 宏:组件内状态 装饰的变量称为状态变量,一旦变量拥有了状态属性,就可以触发其直接绑定 UI 组件的刷新。@State 是状态变量相关宏中最基础的宏,也是大部分状态变量的数据源。
- 同步类型:不与父组件中任何类型的变量同步。
- 传递规则:从父组件初始化可选(传入的值会覆盖本地初始化);支持初始化子组件的常规变量、@State、@Link、@Prop、@Provide;不支持组件外访问(私有)。
- 允许装饰的变量类型:基础数据类型(String、Int64、Float64、Bool 可缺省类型)、Enum、Option、struct(struct 内部无法修改)、class(需配合 @Observed/@Publish 感知内部变化)、数组(感知内部变化需使用 ObservedArrayList<T>)、Color;不支持 Any。
- 观察变化:基础类型可观察到数值变化;struct 只能整体赋值(属性赋值编译报错);class 的整体赋值、属性赋值、嵌套属性赋值均可感知;ObservedArrayList 的数组项赋值、增删数组项、嵌套属性赋值均可感知;Array、Varray、ArrayList、HashMap、HashSet 等普通 Collection 仅支持整体赋值新数组,无法监听内部元素变化。
- 框架行为:状态变量改变时,查询依赖该状态变量的组件 → 执行依赖组件的更新方法 → 与状态变量不相关的组件或 UI 描述不重新渲染,实现按需更新。
- 限制条件:@State 变量必须初始化(否则编译期报错);不支持装饰 Function 类型变量。
注意: 状态变量只能影响其直接绑定的 UI 组件的刷新;且不允许在
build里改变状态变量,否则可能引发组件无限循环重渲染直至 appfreeze。
@Prop:父子单向同步
@Prop 宏:父子单向同步 装饰的变量可以和父组件建立单向同步关系:@Prop 变量允许在本地修改,但修改后的变化不会同步回父组件;当数据源更改时,@Prop 变量会更新并覆盖本地所有更改。
- 限制条件:仅可在子组件中定义,不能在 @Entry 装饰的自定义组件中使用;只能用
var声明且必须指明类型;禁止本地初始化,必须从父组件初始化;类型需与数据源一致,且数据源必须是宏装饰的状态变量(如 @State)。 - 传递规则:支持父组件中的常规变量、@State、@Link、@Prop、@Provide、@Consume、@Publish、@StorageLink、@StorageProp、@LocalStorageLink、@LocalStorageProp 初始化子组件的 @Prop;@Prop 也可用于初始化子组件的常规变量、@State、@Link、@Prop、@Provide。
- 框架行为:初始渲染时,父组件
build()创建子组件新实例并将数据源传入,初始化 @Prop 变量;更新时,@Prop 的更新仅停留在当前子组件,父组件数据源更新则会重置并覆盖 @Prop 的所有本地修改。 - 性能提示:在组件复用场景,建议 @Prop 深度嵌套数据不超过 5 层,嵌套太多会导致深拷贝占用空间过大并引发垃圾回收性能问题。
@Prop 的典型使用场景包括:父组件 @State 到子组件 @Prop 简单数据类型同步、父组件 @State 数组项到子组件 @Prop 同步、@State 类对象属性到 @Prop 简单类型同步、@Prop 嵌套场景(每一层类都要用 @Observed 装饰且每一层都要被 @Prop 接收)。
@Link:父子双向同步
@Link 宏:父子双向同步 装饰的变量与其父组件中对应的数据源建立双向数据绑定,双方共享相同的值:其中一方改变时,另外一方能够感知到变化。
- 限制条件:仅可在子组件中定义;只能用
var声明且必须指明类型;禁止本地初始化(@Link var count: Int64 = 10编译报错);类型必须与数据源类型一致;数据源必须是宏装饰的状态变量(不能用常量或常规变量初始化);不支持装饰 Function 类型变量。 - 框架行为(以父组件为 @State 为例):
- 初始渲染时,子组件 @Link 包装类拿到父组件的 @State 状态变量后,将当前 @Link 包装类 this 指针注册给父组件的 @State 变量;
- 父 @State 变更后,遍历更新所有依赖它的系统组件(elementId)和状态变量(如 @Link 包装类),从而同步到子组件;
- 子组件 @Link 更新后,调用父 @State 包装类的 set 方法将数值同步回父组件,双方分别遍历依赖的系统组件进行 UI 更新。
- 使用场景:简单类型和类对象类型(class 需 @Observed/@Publish 修饰方可观察属性变化)、ObservedArrayList 数组类型(可感知元素添加、删除和替换)、借助 @Watch 在双向同步时更改本地其他变量。
@Provide/@Consume:与后代组件双向同步
@Provide 宏和 @Consume 宏:与后代组件双向同步 用于状态数据在多个层级之间的传递场景。与父子组件通过命名参数机制传递不同,@Provide/@Consume 摆脱参数传递机制的束缚,实现跨层级传递:
- @Provide 装饰的变量在祖先组件中,自动对其所有后代组件可用;@Consume 装饰的变量在后代组件中,"消费(绑定)"祖先组件提供的变量。二者建立跨组件层级的双向同步。
- 绑定方式:通过相同的变量名或相同的变量别名绑定,且类型必须相同。同一自定义组件内声明多个同名/同别名的 @Provide 时,@Consume 会向上查找匹配最近的 @Provide。
- 宏说明:@Provide 必须指明类型、指定初始值,支持重名;@Consume 必须指明类型、不能有初始值;二者的宏参数(别名)均为可选常量字符串。
- 允许装饰的类型更广:仓颉内置类型(基础数据类型及自定义类型及数组)、函数类型、DateTime 等。
- 观察变化:除基础类型外,可观察 HashMap(通过 set、clear、remove 接口更新)、HashSet(通过 add、clear、remove 接口更新)、DateTime(通过 addDays、addHours 等接口更新)的变化,也可装饰函数类型变量并观察到函数输出值的变化。
- 框架行为:初始渲染时 @Provide 变量以 Map 形式传递给所有子组件,@Consume 在 Map 中查找对应变量并把自己注册给 @Provide;任一方变化都会遍历更新依赖的组件,实现双向同步。
- 限制条件:key 必须为 String 字面量;@Consume 不能本地初始化、也不能在构造参数中传入初始化;如果找不到对应 key 的 @Provide,框架会抛出运行时错误。
@Observed/@Publish:嵌套类对象属性变化
@Observed 宏和 @Publish 宏:嵌套类对象属性变化 配套使用,用于类类型属性变化的观察,弥补其他宏仅能观察一层的能力限制——实际开发中应用会封装自己的数据模型(数组、自定义类或自定义类数组),其成员变量的属性变化默认是无法观察到的。
- @Observed 是类宏:只能装饰仓颉 class 类型,需放在 class 定义前;禁止在类定义时继承其他类扩展和接口;禁止修饰 open class;禁止自定义构造函数(@Observed 会自动生成带命名参数的构造函数)。
- @Publish 是属性宏:只能修饰仓颉自定义类型的成员变量(建议为 @Observed 装饰 class 的成员变量),且必须由
var声明、必须指明类型和初始值(String/Int64/Float64/Bool 的初始值为字面量时可省略类型);禁止修饰静态变量和let变量。 - 观察变化:@Observed 装饰的类,若其成员为非简单类型(如 class 或数组),该 class 类型也需要被 @Observed 装饰、数组建议使用 ObservedArrayList,否则观察不到其属性变化。@Publish 修饰被 @Observed 装饰 class 的成员变量,其赋值变化可以被观察到。
- 框架行为:@Observed 装饰的类会自动继承 ObservedObject,自动生成构造函数并创建 setter/getter 绑定的触发事件;读取状态时 ObservedProperty 的 get 函数记录相关组件 id,改变时 set 函数遍历依赖组件通知更新并重新渲染 UI。
关键边界: 仅 @Observed/@Publish 可以观察嵌套场景,其他状态变量仅能观察第一层(各装饰器的"观察变化和行为表现"小节有详细说明)。
管理应用拥有的状态(应用级)
组件级装饰器仅能在页面内(一个组件树)共享状态变量。要实现应用级或多个页面的状态数据共享,需要使用应用级状态管理能力。框架提供了四种能力,整体关系为:AppStorage 是应用状态的中枢,LocalStorage 是页面级存储,PersistentStorage 与 Environment 通过 AppStorage 中转与 UI 交互。
- LocalStorage:页面级 UI 状态存储,通常用于 UIAbility 内、页面间的状态共享。
- AppStorage:特殊的单例 LocalStorage 对象,由 UI 框架在应用程序启动时创建,为应用程序 UI 状态属性提供中央存储。
- PersistentStorage:持久化存储 UI 状态,通常和 AppStorage 配合使用,将选定的 AppStorage 数据写入磁盘,确保属性在应用重启后保持相同值。
- Environment:应用程序运行设备的环境参数,环境参数会同步到 AppStorage 中,与 AppStorage 搭配使用。
AppStorage:应用全局的 UI 状态存储
AppStorage 是应用全局的 UI 状态存储,与应用的进程绑定,由 UI 框架在应用程序启动时创建,为应用级状态数据提供中央存储。属性通过唯一的键字符串值访问。AppStorage 是应用状态的"中枢":持久化数据 PersistentStorage 和环境变量 Environment 都通过 AppStorage 中转,才可以和 UI 交互。
- @StorageProp(key):与 AppStorage 中 key 对应的属性建立单向同步。AppStorage 给定 key 的属性一旦变化,会同步给 @StorageProp 并覆盖本地修改。
- @StorageLink(key):与 AppStorage 中 key 对应的属性建立双向同步。本地修改写回 AppStorage,AppStorage 修改同步到所有绑定该 key 的属性(单向 @StorageProp、双向 @StorageLink 及其他实例如 PersistentStorage)。
- 宏使用规则:key 为必填常量字符串(需要有引号);允许装饰的类型为 class、String、整数、浮点、Bool、enum 及其数组,支持 Datetime、Map、Set;类型必须被指定,且建议与 AppStorage 中对应属性类型相同(否则发生类型隐式转换导致行为异常);不支持 Any;@StorageProp/@StorageLink 不支持装饰 Func 类型变量(运行时错误)。
- 被装饰变量必须指定初始值:由于应用逻辑差异,无法确认组件初始化前 AppStorage 中一定存在对应 key 的属性,因此本地初始化是必要的——若 AppStorage 实例中不存在该属性,则用初始值初始化并存入 AppStorage。
- 限制与建议:AppStorage 同一进程内共享;仅支持纯仓颉场景,不支持 ArkTS 与仓颉混合开发;与 PersistentStorage、Environment 配合时注意调用顺序(先 persistProp/envProp 再访问 AppStorage 同名属性);不建议借助 @StorageLink 的双向同步机制实现事件通知(会触发多余 UI 刷新,推荐使用 emitter 方式)。
LocalStorage:页面级 UI 状态存储
LocalStorage 是页面级的 UI 状态存储,是仓颉为构建页面级别状态变量提供的内存内"数据库":
- 应用程序可以创建多个 LocalStorage 实例,实例可以在页面内共享,也可以实现跨页面、UIAbility 实例内共享。
- 组件树的根节点(被 @Entry 装饰的 @Component)可以被分配一个 LocalStorage 实例,此组件的所有子组件实例自动获得对该实例的访问权限;未被 @Entry 装饰的组件不可被独立分配 LocalStorage 实例。
- LocalStorage 的生命周期由应用程序决定:当应用释放最后一个指向 LocalStorage 的引用(如销毁最后一个自定义组件)时,LocalStorage 将被垃圾回收。LocalStorage 中的所有属性都是可变的,且创建后命名属性的类型不可更改。
LocalStorage 提供两个宏与组件联动:
- @LocalStorageProp(key):单向同步,LocalStorage 中 key 对应属性值改变(如通过 set 接口)会同步给 @LocalStorageProp 并覆盖本地值。
- @LocalStorageLink(key):双向同步,本地修改写回 LocalStorage,LocalStorage 修改同步到所有绑定该 key 的属性(单向 @LocalStorageProp 与双向 @LocalStorageLink)。
使用场景包括:应用逻辑直接操作 LocalStorage 实例(setOrCreate、get、link 等接口)、从 UI 内部使用 @LocalStorageProp/@LocalStorageLink、兄弟组件之间同步状态变量、将 LocalStorage 实例从 UIAbility 共享到一个或多个视图(在 UIAbility 中创建实例,页面通过 @Entry[storage] 获取共享实例),以及装饰 DateTime、Map、Set 类型变量和自定义组件外改变状态变量。
PersistentStorage:持久化存储 UI 状态
PersistentStorage 是应用程序中的可选单例对象,用于持久化存储选定的 AppStorage 属性,确保这些属性在应用重新启动时的值与关闭时的值相同。UI 和业务逻辑不直接访问 PersistentStorage 中的属性,所有属性访问都是对 AppStorage 的访问,AppStorage 中的更改会自动同步到 PersistentStorage。
- 允许的类型:Bool、String、整数、浮点等简单类型;可以被
JSON.stringify()和JSON.parse()重构的对象(对象的成员方法不支持持久化)。 - 不允许的类型:嵌套对象(对象数组、对象的属性是对象等),因为框架目前无法检测 AppStorage 中嵌套对象(包括数组)值的变化,无法写回 PersistentStorage。
- 性能注意事项:持久化数据是相对缓慢的同步操作,应避免持久化大型数据集和经常变化的变量;持久化变量建议小于 2KB,大量数据请使用数据库 API,避免同步磁盘读写阻塞 UI 线程影响渲染性能。
- 调用顺序:必须先在 PersistentStorage 中调用
persistProp,再访问 AppStorage 中的属性。反例是先AppStorage.setOrCreate("aProp", 47)再PersistentStorage.persistProp("aProp", 48)——这会把上次退出应用时持久化的值覆盖丢失。 - 初始化流程:新装应用首次启动时,persistProp 查询磁盘不存在该属性,则在 AppStorage 中创建并写入默认值,同时持久化到磁盘;后续启动时,从磁盘读回的值写入 AppStorage,供 @StorageLink/@StorageProp 绑定使用。
Environment:设备环境查询
Environment 是 ArkUI 框架在应用程序启动时创建的单例对象,用于获取应用程序运行设备的环境参数(如多语言、深浅色模式等),并将其值写入 AppStorage。Environment 的所有属性都是不可变的(应用不可写入)、都是简单类型,开发者需通过 AppStorage 才能获取环境变量值。
Environment 内置参数:
| 键 | 数据类型 | 描述 |
|---|---|---|
| accessibilityEnabled | Bool | 获取无障碍屏幕读取是否启用。 |
| colorMode | ColorMode | 色彩模型类型:ColorMode.Light(浅色)、ColorMode.Dark(深色)。 |
| fontScale | Float64 | 字体大小比例,初始值跟随系统默认参数(需配置 configuration 的 fontSizeScale 为 "followSystem")。 |
| fontWeightScale | Float64 | 字体粗细程度,初始值跟随系统默认参数(不同系统/机型取值范围可能不同)。 |
| layoutDirection | LayoutDirection | 布局方向类型:LayoutDirection.Ltr(从左到右)、LayoutDirection.Rtl(从右到左)。 |
| languageCode | String | 当前系统语言值,取值必须为小写字母(例如:zh),初始值跟随系统默认参数。 |
设备环境到 Component 的更新链为:Environment → AppStorage → Component。使用方式:调用 Environment.envProp<String>("languageCode", "en") 将环境变量存入 AppStorage,再在自定义组件中通过 @StorageProp["languageCode"] let languageCode: String = "en" 读取。注意 @StorageProp 关联的环境参数可以在本地更改,但不能同步回 AppStorage(应用对环境变量参数不可写,只能在 Environment 中查询)。
@Watch:状态变量更改通知
@Watch 宏:状态变量更改通知 用于监听状态变量的变化,是 V1 提供的补充能力。当被监听的状态变量变化时,@Watch 的回调方法将被调用。@Watch 仅能监听到可以观察到的变化,框架内部使用严格相等(==)判断数值有无更新,判断结果为 false 时触发回调。
- 宏说明:宏参数必填(回调方法名);宏顺序不影响功能(建议 @State、@Prop、@Link 等在 @Watch 之前保持风格一致);触发时机为变量真正变化、被赋值的时间。
- 行为表现:观察到状态变量变化(包括双向绑定的 AppStorage 和 LocalStorage 中对应 key 的变化)时回调被触发;回调在自定义组件属性变更之后同步执行;回调里改变其他状态变量也会引起状态变更和 @Watch 执行;第一次初始化时不会调用 @Watch 回调。
- 限制条件:避免在回调方法里直接或间接修改同一个状态变量(会造成无限循环);回调应仅执行快速运算(属性值更新函数会延迟组件重新渲染);不建议在回调中调用异步操作;参数必须是声明的方法名,否则编译期报错;常规变量不能被 @Watch 装饰。
学习路径与推荐阅读
状态管理是 ArkUI 声明式范式中的核心环节。在阅读状态管理文档前,开发者需要对 UI 范式基本语法有基本了解,建议按以下顺序提前阅读:
深入进阶可按本文梳理的体系继续阅读:
- 组件级:@State → @Prop → @Link → @Provide/@Consume → @Observed/@Publish
- 应用级:管理应用拥有的状态概述 → AppStorage → LocalStorage → PersistentStorage → Environment
- 监听能力:其他状态管理概述 → @Watch
总结
仓颉 ArkUI 状态管理 V1 的核心思想是"UI 是状态的运行结果":开发者通过装饰器让变量拥有状态属性,状态变量的变化触发组件按需刷新。从影响范围看,@State、@Prop、@Link、@Provide/@Consume、@Observed/@Publish 负责组件树内的状态观察与同步(其中仅 @Observed/@Publish 支持嵌套场景观察),AppStorage、LocalStorage、PersistentStorage、Environment 负责页面级与应用级的状态共享与持久化,@Watch 则提供了状态变化的监听回调能力。掌握本文的总览体系,再按学习路径深入各装饰器细节,即可在真实业务中灵活组合这些能力,实现数据和 UI 的高效联动。