仓颉 ArkUI 组件级变量状态管理:ObservedProperty 与 ObservedArrayList 完全指南
仓颉 ArkUI 组件级变量状态管理:ObservedProperty 与 ObservedArrayList 完全指南
在仓颉语言开发的 ArkUI 声明式 UI 框架中,组件级变量的状态管理是驱动界面按需刷新的核心机制。本文围绕参考文档 cj-state-rendering-componentstatemanagement.md 展开,系统讲解状态管理属性类型 ObservedProperty<T> 与状态管理数组列表类型 ObservedArrayList<T> 的完整 API、底层行为与并发安全约束。阅读本文后,你将掌握如何让数组内部元素的新增、删除、修改实时触发 UI 更新,并能够在 spawn 并发场景下安全地修改状态变量。
一、组件级状态管理概述
在声明式 UI 编程框架中,UI 是程序状态的运行结果:状态变量值的改变会引起 UI 重新渲染,这一机制在仓颉中统称为状态管理(详见 状态管理概述)。
普通的 ArrayList、Array 等容器虽然可以作为 @State 等装饰器的数据源,但只能感知数组整体的重新赋值,无法感知内部元素的变化——例如 this.arrlist<a href="https://link.gitcode.com/i/73ae01981a7aecb9fa4e6ecd75cfe8fc" target="_blank">0] = 10 这类数组项修改不会触发 UI 刷新(见 [@State 宏文档)。为此,ArkUI 为仓颉提供了专门的组件级状态管理类型:
ObservedProperty<T>:用于状态管理的属性类型;ObservedArrayList<T>:用于状态管理的数组列表类型,当其内部数组发生变化时(修改其中一项的值、删除或添加一项),会立即触发 UI 更新。
适用前提:本文 API 的系统能力为
SystemCapability.ArkUI.ArkUI.Full,起始版本为 22,仅支持在 UI 主线程使用,不能在子线程、worker、taskpool 中使用。
二、导入模块
所有相关 API 均通过 kit.ArkUI 包导入:
import kit.ArkUI.*
若在示例中同时使用 @State、@Observed 等状态管理宏,还需要导入宏管理模块:
import ohos.arkui.state_macro_manage.*
三、class ObservedProperty<T>:状态管理属性类型
public open class ObservedProperty<T> {
}
ObservedProperty<T> 表示用于进行状态管理的属性类型,是状态管理框架内部数据包装的核心类型。它也是应用级状态管理 API 的返回值类型:
- AppStorage.link<T> 返回
?ObservedProperty<T>,与 AppStorage 中的属性建立双向绑定; - AppStorage.property<T> 返回
?ObservedProperty<T>,建立单向绑定。
从源码结构可以推断,ObservedProperty 内部包装了状态变量本身,并负责维护"依赖该状态的组件 id"这一订阅关系:当状态变量被读取时,触发 get 函数记录使用它的组件;当状态变量被修改时,触发 set 函数遍历依赖它的组件并通知其重新渲染。这正是 @Observed 宏和 @Publish 宏 中"框架行为"一节所描述的观察机制。
四、class ObservedArrayList<T>:状态管理数组列表类型
ObservedArrayList<T> 是本文的核心类型,用于替代普通数组,实现对数组内部变化(增、删、改、清空等)的观察。
public class ObservedArrayList<T> <: CollectionEx<T> {
public init(ArrayList<T>)
public init(Array<T>)
}
其父类型为 CollectionEx<T>,该接口定义了集合的公共访问协议:
public interface CollectionEx<T> {
prop size: Int64
operator func [](idx: Int64, value!: T): Unit
operator func [](idx: Int64): T
}
同时,Array<T> 与 ArrayList<T> 都被扩展为 CollectionEx<T> 的子类型(见 cj-common-types.md),这正是 ObservedArrayList 两种构造函数都能接受 Array 与 ArrayList 的原因,也是 ForEach 组件能直接以 CollectionEx<T> 为数据源进行循环渲染的原因。
4.1 属性与构造函数
| 成员 | 签名 | 说明 |
|---|---|---|
prop size |
public prop size: Int64 |
获取状态管理数组列表的大小,只读。 |
init(ArrayList<T>) |
public init(initValue: ArrayList<T>) |
用 ArrayList<T> 初始化一个 ObservedArrayList。initValue 为必填初始化值。 |
init(Array<T>) |
public init(initValue: Array<T>) |
用 Array<T> 初始化一个 ObservedArrayList。initValue 为必填初始化值。 |
两种构造函数均支持省略类型参数的便捷写法,例如仓库中 ForEach 渲染控制文档 中的用法:
// 由 Array 字面量初始化
@State var simpleList: ObservedArrayList<String> = ObservedArrayList<String>(['one', 'two', 'three'])
// 由 ArrayList 初始化
@State var title: ObservedArrayList<Model> = ObservedArrayList<Model>(ArrayList<Model>([Model(value: 11), Model(value: 1)]))
4.2 元素读写:下标运算符
// 读取:通过索引获取数组列表中的元素
public operator func [](index: Int64): T
// 写入:通过索引设置数组列表中的元素
public operator func [](index: Int64, value!: T): Unit
- 读取运算符返回
T类型元素; - 写入运算符中
value是命名参数(签名中的!表示命名参数),调用时需写作list[index] = value的形式,例如this.arrlist[0] = 10; - 通过下标对元素赋值属于"数组项修改",能够被
ObservedArrayList观察并触发 UI 刷新。
4.3 增删改方法全集
下表完整列出 ObservedArrayList<T> 提供的增删改方法与返回值,全部起始版本为 22,系统能力为 SystemCapability.ArkUI.ArkUI.Full:
| 方法 | 签名 | 功能 |
|---|---|---|
isEmpty() |
public func isEmpty(): Bool |
判断数组列表是否为空,返回 Bool。 |
clone() |
public func clone(): ObservedArrayList<T> |
克隆数组列表,返回新的 ObservedArrayList<T>。 |
clear() |
public func clear(): Unit |
清空数组列表。 |
append(T) |
public func append(element: T): Unit |
在末尾添加单个元素。 |
appendAll(Collection<T>) |
public func appendAll(elements: Collection<T>): Unit |
在末尾批量添加元素集合。 |
insert(Int64, T) |
public func insert(index: Int64, element: T): Unit |
在指定索引位置插入单个元素。 |
insertAll(Int64, Collection<T>) |
public func insertAll(index: Int64, elements: Collection<T>): Unit |
在指定索引位置批量插入元素。 |
prepend(T) |
public func prepend(element: T): Unit |
在开头添加单个元素。 |
prependAll(Collection<T>) |
public func prependAll(elements: Collection<T>): Unit |
在开头批量添加元素。 |
remove(Int64) |
public func remove(index: Int64): T |
删除指定索引的元素并返回被删除元素。 |
remove(Range<Int64>) |
public func remove(range: Range<Int64>): Unit |
删除指定范围的元素(例如 0..3)。 |
removeIf((T) -> Bool) |
public func removeIf(predicate: (T) -> Bool): Unit |
根据条件谓词删除满足条件的元素。 |
提示:
removeIf的谓词predicate: (T) -> Bool返回true表示该元素应被删除,例如list.removeIf({ item => item.value > 10 })。
4.4 为什么需要 ObservedArrayList:与普通数组的对比
从 @State 宏文档 可以确认如下观察能力差异:
| 数据类型 | 整体赋值 | 数组项修改 | 新增/删除项 |
|---|---|---|---|
Array、Varray、ArrayList、HashMap、HashSet |
✅ 可感知 | ❌ 不可感知 | ❌ 不可感知 |
ObservedArrayList<T> |
✅ 可感知 | ✅ 可感知 | ✅ 可感知 |
例如:
// 普通 ArrayList:数组项赋值无法被感知
@State var arrlist: ArrayList<Int16> = ArrayList<Int16>([1, 2, 3])
this.arrlist[0] = 10 // UI 不刷新
// ObservedArrayList:数组项赋值、增删均可被感知
@State var title: ObservedArrayList<Model> = ObservedArrayList<Model>(ArrayList<Model>([Model(value: 11)]))
this.title[0] = Model(value: 2) // 数组项赋值,UI 刷新
this.title.append(Model(value: 12)) // 新增,UI 刷新
this.title.remove(0) // 删除,UI 刷新
五、ObservedArrayList 在状态管理宏中的定位
5.1 作为 @State / @Link / @Prop 的数据源
ObservedArrayList 可直接被 @State、@Link、@Prop 等状态装饰器修饰,作为父组件与子组件间同步的数组数据源:
- 在 数组类型的 @Link 示例 中,子组件通过
@Link var items: ObservedArrayList<Int>与父组件@State var arr: ObservedArrayList<Int>建立双向同步,items.append(...)、items = ObservedArrayList<Int>([100,200,300])均可被观察并刷新 UI; @Prop同样支持ObservedArrayList<T>类型的单向同步(见 cj-macro-prop.md)。
5.2 与 @Observed / @Publish 配合观察数组项属性
若 ObservedArrayList 的数组项是自定义 class 类型,则需要配合 @Observed 与 @Publish 才能观察到数组项内部属性的赋值变化(见 @Observed 宏和 @Publish 宏):
@Observed
class Model {
@Publish public var value: Int
}
// 数组项中属性的赋值可以感知
@State var title: ObservedArrayList<Model> = ObservedArrayList<Model>(ArrayList<Model>([Model(value: 11), Model(value: 1)]))
this.title[0].value = 6 // UI 刷新
5.3 与 ForEach 循环渲染配合
ObservedArrayList 常与 ForEach 组件配合实现列表的动态渲染。由于 ForEach 生成的 item 是常量,若直接修改普通数组中 item 的内容无法触发 UI 刷新;改用 ObservedArrayList 并配合 @Publish 修饰自定义类属性后,即可实现可观测能力(详见 状态管理合理使用开发指导 中的 "在 ForEach 中使用 ObservedArrayList" 章节):
@Observed
class TextStyles {
@Publish var fontSize: Int64
}
@Entry
@Component
class EntryView {
@State
var styleList: ObservedArrayList<TextStyles> = ObservedArrayList<TextStyles>([])
public override func aboutToAppear() {
for (i in 1..=35) {
this.styleList.append(TextStyles(fontSize: i))
}
}
func build() {
Column {
List() {
ForEach(this.styleList, {item: TextStyles, _: Int64 =>
ListItem() {
Text("Hello World").fontSize(item.fontSize)
}
})
}
}
}
}
5.4 MVVM 模式下的典型用法
在 MVVM 模式 的 ViewModel 层中,ObservedArrayList 是承载列表型 ViewModel 数据的标准容器,例如备忘录示例中:
@Observed
public class TodoListViewModel {
@Publish public var thingViewModelArray: ObservedArrayList<ThingViewModel> = ObservedArrayList<ThingViewModel>()
public func loadTasks() {
let todoList = TodoListModel([])
todoList.loadTasks()
for (thing in todoList.things) {
let thingsViewModel = ThingViewModel()
thingsViewModel.updateTask(thing)
this.thingViewModelArray.append(thingsViewModel)
}
}
public func chooseAll(): Unit {
this.isChoosen = !this.isChoosen
for (i in 0..this.thingViewModelArray.size) {
this.thingViewModelArray[i].isFinish = this.isChoosen
}
}
}
该示例同时演示了 ObservedArrayList 的三类典型操作:append 追加、size 遍历、下标赋值 []。
六、并发场景下的状态更新:spawn 与 launch
状态更新有一个重要注意事项:不允许在 spawn 表达式中对状态变量进行并发修改,会导致并发安全问题。因为 UI 相关逻辑代码必须运行在拥有独立 OS 线程的 UI 线程上,而 spawn 创建的用户态线程与 OS 线程不存在显式绑定关系,无法保证 UI 代码运行在 UI 线程(见 线程控制)。
建议做法:当需要修改状态变量时,采用 concurrency 包提供的 launch 方法,将状态更新的步骤放回主线程中运行,以保证并发安全。以下示例演示如何在 spawn 表达式中安全地更新变量状态(原文完整示例):
package ohos_app_cangjie_entry
import kit.ArkUI.*
import ohos.arkui.state_macro_manage.*
@Entry
@Component
class EntryView {
@State var text: String = "begin"
func build() {
Column(space: 30) {
Button(text).onClick({ evt =>
changeText({ p: String =>
// 使用launch表达式在主线程中更新状态变量
launch {
text = p
}
})
})
}.width(100.percent)
}
private func changeText(callback: (String) -> Unit): Unit {
spawn {
while (true) {
callback("blink 0")
sleep(Duration.millisecond * 100)
callback("blink 1")
sleep(Duration.millisecond * 100)
}
}
}
}
代码逻辑拆解:
changeText内部开启一个spawn协程,循环回调callback,交替传入"blink 0"/"blink 1",每次间隔100ms;- 点击
Button时,回调闭包内通过launch { text = p }将状态变量text的赋值操作调度回主线程执行; - 状态变量
text在 UI 线程上被修改,Button的文案随之安全刷新,避免了跨线程修改状态导致的并发安全问题。
七、使用注意事项与最佳实践
- UI 主线程约束:状态管理功能仅支持在 UI 主线程使用,不能在子线程、worker、taskpool 中使用(见 状态管理概述 的说明)。
- 禁止在 build 中修改状态变量:在
build方法内改变状态变量会引发组件无限循环重新渲染,严重时可导致应用无响应(appfreeze),详见 @State 宏文档 的常见问题章节。 - 用临时变量替换状态变量:频繁直接赋值状态变量会触发 ArkUI 多次查询与渲染;建议先在临时变量中完成计算,最后一次写回状态变量,可显著降低耗时(见 状态管理优秀实践)。
- 避免在循环中频繁读取状态变量:应将状态变量的读取移出循环体,减少不必要的查询行为。
- 数组项为自定义类型时:必须同时使用
@Observed(修饰 class)与@Publish(修饰成员变量),否则ObservedArrayList只能感知项的增删改,无法感知项内部属性的变化。 - 合理控制对象类型状态变量关联的组件数:复杂对象任一部员属性变化会导致其关联的所有组件刷新,建议合理拆分对象,避免冗余刷新。
八、相关文档索引
- 组件级状态管理参考:cj-state-rendering-componentstatemanagement.md(本文主题文档)
- 应用级状态管理参考:cj-state-rendering-appstatemanagement.md
- 公共类型
CollectionEx:cj-common-types.md - 循环渲染组件 ForEach:cj-state-rendering-foreach.md
- 状态管理宏:@State、@Link、@Prop、@Observed/@Publish
- 状态管理实战:@Observed/@Publish 使用示例、MVVM 模式、状态管理优秀实践、状态管理合理使用开发指导
- 线程控制:cj-appendix-thread.md