ArkUI V2 状态管理极简教程,手写待办清单 Demo

很多刚接触鸿蒙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对状态管理做了底层重构,核心优势可以总结为三点:

  1. 嵌套自动监听 :支持多层嵌套对象的属性监听,无需层层传递@ObjectLink
  2. 细粒度更新 :通过@Trace精准标记需要监听的属性,未标记属性变化不会触发UI重绘
  3. 代码更简洁:减少大量样板代码,数组元素内部属性修改直接生效,无需手动刷新

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 功能清单

  1. 输入框输入待办内容,点击按钮添加到列表
  2. 点击待办条目切换完成状态,已完成条目显示删除线
  3. 点击删除按钮移除对应待办
  4. 底部实时统计已完成数量与总数量

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 运行验证

将代码复制到工程中,运行到模拟器或真机,即可验证完整功能:

  1. 输入文字点击添加,列表实时新增条目
  2. 点击左侧圆形按钮,待办自动切换为已完成状态,文字出现删除线
  3. 点击删除按钮,条目从列表中移除
  4. 底部统计数字实时更新

核心亮点: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状态管理体系。

相关推荐
水龙吟啸2 小时前
华为2026.6.17机考选择题+编程题【速刷敲黑板】
人工智能·深度学习·算法·华为
ujainu小2 小时前
帧率协议栈:13变量揭秘60Hz性能分析
华为·harmonyos
花开彼岸天~10 小时前
15 Button 组件与交互反馈设计:按压动画与呼吸脉冲
华为·交互·harmonyos·鸿蒙系统
ZZZMMM.zip16 小时前
基于鸿蒙HarmonyOS NEXT开发AI英语口语应用:智能口语练习新体验与鸿蒙Flutter框架跨端实
人工智能·flutter·华为·harmonyos·鸿蒙·鸿蒙系统
心中有国也有家16 小时前
AtomGit Flutter 鸿蒙客户端:白噪音场景的视觉设计
学习·flutter·华为·harmonyos
最爱老式锅包肉16 小时前
《HarmonyOS技术精讲-ArkGraphics 2D(方舟2D图形服务)》第九篇:数据可视化——柱状图与饼图的绘制实战
华为·信息可视化·harmonyos
心中有国也有家17 小时前
AtomGit Flutter 鸿蒙客户端:呼吸练习的完整生命周期
学习·flutter·华为·harmonyos
绝世番茄18 小时前
登录表单布局:从 Column 到完整表单 —— 鸿蒙 HarmonyOS ArkTS 原生学习指南
华为·list·harmonyos·鸿蒙
不才难以繁此生18 小时前
HarmonyOS ArkTS 实战|中式美食用户行为数据:Preferences 持久化、搜索缺口与候选排序
harmonyos·arkts·用户行为分析·preferences·中式美食·arkui v2