仓颉 ArkUI 状态管理(V1)机制全解:从状态变量到应用级状态存储

原创2026-10-15 15:44:191,866 阅读
文章标签:文档知识库

仓颉 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 为例):
    1. 初始渲染时,子组件 @Link 包装类拿到父组件的 @State 状态变量后,将当前 @Link 包装类 this 指针注册给父组件的 @State 变量;
    2. 父 @State 变更后,遍历更新所有依赖它的系统组件(elementId)和状态变量(如 @Link 包装类),从而同步到子组件;
    3. 子组件 @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 范式基本语法有基本了解,建议按以下顺序提前阅读:

深入进阶可按本文梳理的体系继续阅读:

总结

仓颉 ArkUI 状态管理 V1 的核心思想是"UI 是状态的运行结果":开发者通过装饰器让变量拥有状态属性,状态变量的变化触发组件按需刷新。从影响范围看,@State、@Prop、@Link、@Provide/@Consume、@Observed/@Publish 负责组件树内的状态观察与同步(其中仅 @Observed/@Publish 支持嵌套场景观察),AppStorage、LocalStorage、PersistentStorage、Environment 负责页面级与应用级的状态共享与持久化,@Watch 则提供了状态变化的监听回调能力。掌握本文的总览体系,再按学习路径深入各装饰器细节,即可在真实业务中灵活组合这些能力,实现数据和 UI 的高效联动。

登录后查看全文
CangjieCorpus