仓颉 ArkUI 组件级变量状态管理:ObservedProperty 与 ObservedArrayList 完全指南

原创2026-10-15 19:48:301,627 阅读
文章标签:文档知识库

仓颉 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 的返回值类型:

从源码结构可以推断,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)
            }
        }
    }
}

代码逻辑拆解:

  1. changeText 内部开启一个 spawn 协程,循环回调 callback,交替传入 "blink 0" / "blink 1",每次间隔 100ms;
  2. 点击 Button 时,回调闭包内通过 launch { text = p } 将状态变量 text 的赋值操作调度回主线程执行;
  3. 状态变量 text 在 UI 线程上被修改,Button 的文案随之安全刷新,避免了跨线程修改状态导致的并发安全问题。

七、使用注意事项与最佳实践

  1. UI 主线程约束:状态管理功能仅支持在 UI 主线程使用,不能在子线程、worker、taskpool 中使用(见 状态管理概述 的说明)。
  2. 禁止在 build 中修改状态变量:在 build 方法内改变状态变量会引发组件无限循环重新渲染,严重时可导致应用无响应(appfreeze),详见 @State 宏文档 的常见问题章节。
  3. 用临时变量替换状态变量:频繁直接赋值状态变量会触发 ArkUI 多次查询与渲染;建议先在临时变量中完成计算,最后一次写回状态变量,可显著降低耗时(见 状态管理优秀实践)。
  4. 避免在循环中频繁读取状态变量:应将状态变量的读取移出循环体,减少不必要的查询行为。
  5. 数组项为自定义类型时:必须同时使用 @Observed(修饰 class)与 @Publish(修饰成员变量),否则 ObservedArrayList 只能感知项的增删改,无法感知项内部属性的变化。
  6. 合理控制对象类型状态变量关联的组件数:复杂对象任一部员属性变化会导致其关联的所有组件刷新,建议合理拆分对象,避免冗余刷新。

八、相关文档索引

登录后查看全文
CangjieCorpus