很多刚接触鸿蒙ArkUI开发的同学,都会遇到一个经典坑:用@State管理对象数组,修改数组里某个对象的内部属性时,UI死活不刷新。旧版方案要么给类加@Observed、配合@ObjectLink层层传递,要么手动替换数组引用强制刷新,代码冗余且容易出现性能问题。

随着鸿蒙7(API 26)全面落地ArkUI V2声明式架构,全新的状态管理体系彻底解决了这个痛点。通过@ObservedV2 + @Trace的组合,我们可以实现细粒度的嵌套对象状态监听,代码更简洁,性能更优异。
本文就以最经典的「待办清单」为Demo,带你零基础上手ArkUI V2状态管理,全程无第三方依赖,代码复制即可运行。
一、ArkUI V2 状态管理:解决了什么核心问题
1.1 旧版状态管理的经典痛点
在ArkUI V1体系中,状态管理依赖@State、@Observed、@ObjectLink等装饰器,在处理嵌套对象、数组元素属性变化时存在明显短板:
@State只能监听变量本身的引用变化,无法监听对象内部属性、数组元素内部属性的变更- 要实现对象属性监听,需要给类加
@Observed装饰,且子组件必须用@ObjectLink接收,层级越深代码越繁琐 - 无法做到属性级的细粒度更新,一个属性变化可能触发整个组件重绘,性能开销大
举个最常见的场景:定义一个Todo待办类,用@State管理Todo数组,当我们点击条目切换isDone状态时,旧版写法下UI不会有任何变化,因为@State监听不到数组元素的内部属性变更。
1.2 V2 状态管理的核心升级
ArkUI V2对状态管理做了底层重构,核心优势可以总结为三点:
- 嵌套自动监听 :支持多层嵌套对象的属性监听,无需层层传递
@ObjectLink - 细粒度更新 :通过
@Trace精准标记需要监听的属性,未标记属性变化不会触发UI重绘 - 代码更简洁:减少大量样板代码,数组元素内部属性修改直接生效,无需手动刷新
1.3 环境要求
- SDK版本:API 12及以上支持,API 26(鸿蒙7)能力最完善
- 开发工具:DevEco Studio 5.0及以上
- 工程模型:Stage模型,ArkTS语言
二、核心装饰器:3个就够覆盖80%场景
ArkUI V2状态管理的装饰器很多,但日常开发最核心的只有3个,掌握它们就能应对绝大多数业务场景。
2.1 @ObservedV2:可观察类装饰器
- 作用 :装饰一个class类,让该类的所有实例具备可观察能力,是V2状态体系的基础,替代旧版的
@Observed装饰器。 - 特点 :只有被
@ObservedV2装饰的类,其属性变化才有可能触发UI更新;普通类即使属性变化,也不会驱动UI刷新。 - 基础用法:
typescript
@ObservedV2
class User {
// 内部属性配合@Trace使用
}
2.2 @Trace:属性级监听标记
- 作用:标记类中的某个属性,当该属性的值发生变化时,自动触发关联的UI组件更新。
- 特点 :精准控制刷新范围,未加
@Trace的属性变化不会触发UI重绘,是性能优化的核心手段;支持深层嵌套,嵌套对象的属性加了@Trace也能被监听。 - 基础用法:
typescript
@ObservedV2
class User {
@Trace name: string = ''; // 变化触发UI更新
age: number = 0; // 变化不触发UI更新
}
2.3 @Local:组件本地状态
- 作用 :声明组件内部的本地状态变量,功能等价于旧版的
@State,是ArkUI V2推荐的组件内状态写法。 - 特点:底层基于V2架构实现,性能更优;支持更灵活的初始化逻辑,变量仅在当前组件内有效,外部无法访问。
- 基础用法:
typescript
@Component
struct Demo {
@Local count: number = 0; // 组件本地状态
}
补充说明:旧版
@State在V2体系中仍然兼容可用,但新项目建议优先使用@Local,享受V2架构的性能红利。
三、实战:从零手写待办清单 Demo
我们将实现一个包含「添加待办、切换完成状态、删除待办、进度统计」的完整待办清单,全程基于ArkUI V2状态管理特性,代码可直接复制运行。
3.1 功能清单
- 输入框输入待办内容,点击按钮添加到列表
- 点击待办条目切换完成状态,已完成条目显示删除线
- 点击删除按钮移除对应待办
- 底部实时统计已完成数量与总数量
3.2 第一步:定义数据模型
这是V2状态管理的核心步骤:用@ObservedV2装饰待办类,给需要监听的属性加上@Trace标记。
可以新建entry/src/main/ets/model/Todo.ets文件,也可以直接写在页面文件顶部:

typescript
@ObservedV2
export class Todo {
@Trace id: number;
@Trace content: string;
@Trace isDone: boolean;
constructor(id: number, content: string) {
this.id = id;
this.content = content;
this.isDone = false;
}
}
关键说明:id、content、isDone三个属性都需要参与UI渲染,因此全部加上
@Trace标记。如果后续增加不需要触发UI更新的属性,不加@Trace即可。
3.3 第二步:页面状态与核心逻辑
打开entry/src/main/ets/pages/Index.ets,先定义页面状态与业务方法:
- 用
@Local管理待办列表与输入框文本 - 实现添加、切换、删除三个核心方法

typescript
import { Todo } from '../model/Todo';
import { promptAction } from '@kit.ArkUI';
@Entry
@Component
struct TodoPage {
// 组件本地状态:待办列表
@Local todoList: Todo[] = [];
// 输入框文本
@Local inputText: string = '';
// 自增ID
private nextId: number = 1;
// 添加待办
addTodo() {
const content = this.inputText.trim();
if (!content) {
promptAction.showToast({ message: '请输入待办内容' });
return;
}
// 新增待办项,加入列表
this.todoList.push(new Todo(this.nextId++, content));
// 清空输入框
this.inputText = '';
}
// 切换待办完成状态
toggleTodo(todo: Todo) {
// 直接修改对象属性,V2会自动触发UI更新
// 旧版写法下这行代码无法触发UI刷新
todo.isDone = !todo.isDone;
}
// 删除待办
deleteTodo(id: number) {
const index = this.todoList.findIndex(item => item.id === id);
if (index !== -1) {
this.todoList.splice(index, 1);
}
}
// 计算已完成数量
get doneCount(): number {
return this.todoList.filter(item => item.isDone).length;
}
3.4 第三步:完整页面UI
接着写build方法,实现完整的界面布局:

typescript
build() {
Column() {
// 顶部标题
Text('待办清单')
.fontSize(28)
.fontWeight(FontWeight.Bold)
.width('90%')
.margin({ top: 40, bottom: 20 });
// 输入区域
Row({ space: 12 }) {
TextInput({ text: this.inputText, placeholder: '输入待办事项' })
.layoutWeight(1)
.height(48)
.borderRadius(24)
.padding({ left: 16, right: 16 })
.backgroundColor('#F5F5F5')
.onChange((value) => {
this.inputText = value;
})
.onSubmit(() => {
this.addTodo();
});
Button('添加')
.width(80)
.height(48)
.borderRadius(24)
.backgroundColor('#0A59F7')
.onClick(() => {
this.addTodo();
});
}
.width('90%')
.margin({ bottom: 20 });
// 待办列表
List({ space: 10 }) {
ForEach(this.todoList, (todo: Todo) => {
ListItem() {
Row({ space: 12 }) {
// 勾选按钮
Button(todo.isDone ? '✓' : '')
.width(24)
.height(24)
.borderRadius(12)
.backgroundColor(todo.isDone ? '#00B578' : '#FFFFFF')
.border({ width: 2, color: '#DDDDDD' })
.fontSize(14)
.fontColor('#FFFFFF')
.onClick(() => {
this.toggleTodo(todo);
});
// 待办内容
Text(todo.content)
.layoutWeight(1)
.fontSize(16)
.fontColor(todo.isDone ? '#999999' : '#333333')
.decoration({ type: todo.isDone ? TextDecorationType.LineThrough : TextDecorationType.None });
// 删除按钮
Button('删除')
.width(60)
.height(32)
.borderRadius(16)
.backgroundColor('#FF4D4F')
.fontSize(14)
.onClick(() => {
this.deleteTodo(todo.id);
});
}
.width('100%')
.padding(16)
.backgroundColor('#FFFFFF')
.borderRadius(12)
.shadow({ radius: 4, color: '#1A000000' });
}
}, (todo: Todo) => todo.id.toString());
}
.layoutWeight(1)
.width('90%');
// 底部统计栏
Row() {
Text(`已完成:${this.doneCount} / 总计:${this.todoList.length}`)
.fontSize(14)
.fontColor('#666666');
}
.width('90%')
.height(50)
.borderRadius(12)
.backgroundColor('#F0F7FF')
.justifyContent(FlexAlign.Center)
.margin({ bottom: 20 });
}
.width('100%')
.height('100%')
.backgroundColor('#F8F9FA');
}
}
3.5 运行验证
将代码复制到工程中,运行到模拟器或真机,即可验证完整功能:
- 输入文字点击添加,列表实时新增条目
- 点击左侧圆形按钮,待办自动切换为已完成状态,文字出现删除线
- 点击删除按钮,条目从列表中移除
- 底部统计数字实时更新
核心亮点:
toggleTodo方法中,我们直接修改了todo对象的isDone属性,没有替换数组引用,也没有使用任何强制刷新手段,UI依然自动更新------这就是ArkUI V2状态管理的能力。
四、直观对比:旧版写法 vs V2写法
我们以「切换待办完成状态」这个最经典的场景为例,直观感受两种写法的差异。
旧版 V1 写法
typescript
// 旧版:需要@Observed装饰类
@Observed
class Todo {
isDone: boolean = false;
content: string = '';
}
@Component
struct TodoItem {
// 子组件必须用@ObjectLink接收,才能监听属性变化
@ObjectLink todo: Todo;
onToggle: () => void;
build() {
// 子组件内修改才能触发刷新
}
}
// 父组件中直接修改数组元素属性,不会触发UI更新
// this.todoList[0].isDone = true 无效
新版 V2 写法
typescript
// V2:@ObservedV2 + @Trace
@ObservedV2
class Todo {
@Trace isDone: boolean = false;
@Trace content: string = '';
}
// 父组件中直接修改即可生效,无需子组件额外处理
this.todoList[0].isDone = true; // UI自动刷新
可以看到,V2写法省去了@ObjectLink的层层传递,也不需要拆分额外的子组件来实现属性监听,代码量大幅减少,逻辑更直观。
五、高频踩坑与避坑指南
1. 属性变化不触发UI更新:漏加@Trace
这是新手最容易踩的坑。@ObservedV2只是让类具备可观察能力,具体哪些属性变化能触发UI更新,完全由@Trace标记决定。如果忘记给属性加@Trace,无论怎么修改,UI都不会刷新。
2. 使用对象字面量代替class实例
@ObservedV2只能装饰class类,普通的对象字面量(const todo = { isDone: false })无法被观察,即使结构完全一致也不行。必须通过new关键字创建类的实例,状态监听才会生效。
3. 混用@Observed与@ObservedV2
旧版@Observed与新版@ObservedV2是两套独立的观察体系,不能混用在同一个类上,否则会出现状态监听异常、UI不刷新等问题。新项目建议统一使用V2体系。
4. 误以为@Local和@State完全等价
二者功能类似,但底层实现不同:@Local基于V2架构,@State基于V1架构。在配合@ObservedV2使用时,@Local的性能与兼容性更好;如果配合旧版@Observed使用,建议保留@State。
六、总结
ArkUI V2状态管理的核心理念是「细粒度、低门槛、高性能」,通过@ObservedV2 + @Trace的组合,彻底解决了旧版嵌套对象监听困难的痛点,同时大幅减少了样板代码。
对于新手来说,掌握本文提到的3个核心装饰器,就可以轻松应对绝大多数业务场景的状态管理需求。待办清单虽然简单,但覆盖了状态管理最核心的「列表渲染、属性修改、增删操作」三大场景,吃透这个Demo,再去开发更复杂的页面就会得心应手。
后续进阶可以继续学习@ParamV2父子传参、@Provide/@ConsumerV2跨组件状态共享等特性,逐步搭建完整的V2状态管理体系。