在这里插入图片描述

一、我们想解决什么问题

1.1 先从生活场景说起

想象一下,你在一个餐厅里点餐。服务员递给你一张纸质菜单,你看了看,说"我要一份宫保鸡丁"。服务员在纸上记下来,然后转身去后厨下单。

这个过程有没有什么问题?有!你坐在那里,根本不知道后厨收到订单没有、菜做好没有、什么时候能上桌。你只能干等着,心里没底。

现在换一种方式:这个餐厅里有一块大屏幕,上面实时显示着每一道菜的状态——“已下单”“制作中”“已完成”“上菜中”。你点完餐,眼睛盯着屏幕,看到状态一步步变化,心里就踏实了。

UI 界面就像这块大屏幕,而数据就是那块不断变化的状态。 屏幕要实时、准确地反映出数据的变化,否则用户就会一脸懵。

1.2 开发中遇到的具体痛点

回到软件开发,我们来还原一个最常见的场景:写一个计数器页面。

页面上有一个数字显示区,有两个按钮——“加"和"减”。点击"加",数字 +1;点击"减",数字 -1。这个需求简单到不能再简单了,但背后有一个核心问题要解决:

当数字变化的时候,界面上的显示怎么跟着变?

如果让我们从零开始实现,你会怎么做?最容易想到的思路是:定义一个变量 count,当用户点击按钮时修改变量 count,然后手动找到页面上显示数字的那个 Text 组件,把新的值塞进去。大致逻辑如下:

定义 count = 0
用户点了"加"按钮
  → count = count + 1
  → 找到页面上显示数字的 Text 组件
  → 调用 Text.setValue(count.toString())

这套思路在 JavaScript 或者一些老式前端框架里确实这么干过,叫"命令式编程"——你得一步一步告诉程序怎么去做。好处是逻辑清晰,坏处是:一旦页面复杂起来,你要手动维护"数据"和"界面"之间的对应关系,这就像在一个有500个员工的工厂里,没有广播系统,老板要一个个通知每个人任务变了——累死而且容易出错。

1.3 更好的方式:声明式编程

ArkUI 采用了"声明式"的思路。它的核心思想是:你只需要告诉程序"数据是什么",界面会自动"声明"成对应的样子。

打个比方。你在餐厅点餐,只需要说"我要宫保鸡丁",不需要告诉服务员怎么炒、怎么端、什么时候翻锅。你描述"要什么",系统负责"怎么做"。

在 ArkUI 里,@State 就是这个机制的关键——它把一个变量"升级"成"受监控的状态"。变量变了,界面自动刷新。这就是我们今天要彻底搞懂的核心工具。

1.4 计数器虽小,五脏俱全

别看计数器功能简单,它几乎涵盖了所有 UI 交互的基本模式:

  • 展示数据count 的当前值显示在页面上
  • 修改数据:点击按钮改变 count 的值
  • 反映变化:界面上显示的数字跟着更新

这三个环节,正好对应了状态管理的三个核心职责:定义状态、修改状态、响应状态变化。 搞懂了这三个环节,任何复杂的 UI 都不在话下——无非就是状态多了一点、变化复杂了一点,原理是一模一样的。


二、数据模型设计

2.1 从真实需求出发

在动手写代码之前,我们先把需求梳理清楚。拿计数器这个场景来说,数据模型其实超级简单——只需要一个数字就够了。但为了让大家感受到 @State 处理更复杂数据的能力,我们把场景稍微扩展一下,做一个"待办事项管理器"。

为什么选这个?因为它涵盖了日常 App 开发中最常见的数据类型:

  • 字符串(任务标题)
  • 布尔值(是否完成)
  • 数组(多个任务)
  • 数字(任务优先级或序号)

这四种类型覆盖了绝大多数 App 的数据场景。学会了用 @State 管理这四种类型,其他场景触类旁通。

2.2 TypeScript Interface 定义

我们用 TypeScript 的 interface 来定义数据模型。ArkUI 的语言是 ArkTS,它是 TypeScript 的超集,所以 TypeScript 的类型定义完全通用。

// 待办事项的数据结构
interface TodoItem {
  id: number;        // 唯一标识
  title: string;     // 任务标题
  completed: boolean; // 是否已完成
  priority: number;  // 优先级(数字越大优先级越高)
}

// 应用整体状态
interface AppState {
  todos: TodoItem[];      // 所有待办事项列表
  inputText: string;     // 用户当前输入的文字
  filter: 'all' | 'active' | 'completed'; // 筛选状态
}

怎么样,是不是跟写 JSON 数据结构差不多?这就是 ArkTS 的优势——类型清晰、一目了然,后面的代码写起来心里有底。

2.3 为什么要先设计数据模型?

很多新手容易犯一个错误:拿到需求就开始写 UI,边写边加字段,想到哪改到哪。这样写出来的代码,数据结构一团糟,后面维护起来欲哭无泪。

正确的做法是:先把"房子"的骨架(数据模型)设计好,再去装修(写 UI)。 数据模型是根本,UI 是表象。用 @State 管理状态,更是要求数据模型清晰——因为 @State 监控的每一个字段,背后都对应着界面上的一个变化点。

2.4 数据模型的演进思路

在实际项目中,数据模型往往不是一步到位的。我建议大家遵循"先跑通、再优化"的策略:

第一阶段(MVP): 先用最简单的数据结构。比如待办事项,只需要一个 title 就够了。先让功能跑起来,验证核心逻辑。

第二阶段(完善): 加上 idcompleted 这些必要字段,让数据有唯一性和状态区分。

第三阶段(扩展): 根据产品需求加上 prioritydueDatetags 等字段,逐步丰富模型的表达能力。

这样做的好处是:每一步都有产出,每一步都能运行。 你不会陷入"设计了一个完美模型但三个月没写一行 UI"的泥潭。


三、核心设计决策

3.1 状态管理的多种方案

在 ArkUI 里,管理状态有多种方式,它们不是非此即彼的关系,而是分层配合使用的。下面用一个表格把它们放在一起比较:

方案 适用场景 作用范围 典型用法
@State 单组件内部状态 仅限当前组件 计数器、输入框
@Prop 父→子单向传值 仅限子组件 配置参数传入
@Link 父子双向同步 仅限子组件 表单联动
@Provide/@Inject 跨层级传值 祖先后代链 全局主题、语言设置
@StorageLink 持久化状态 应用全局 用户偏好设置
@Observed + @ObjectLink 嵌套对象监听 对象层级 复杂业务对象

这张表初看有点吓人——怎么有这么多方案?先别慌。我们只要记住一个原则:按需选择,从简到繁。 能用一个 @State 解决的事,不要一开始就上 AppStorage

3.2 选型理由

我们这个"待办事项管理器"的核心场景,选用 @State 作为主方案,理由如下:

第一,这是单组件内部管理。 所有数据(待办列表、输入框内容、筛选状态)都在同一个页面/组件内流转,不需要跨组件共享,所以用 @State 完全够用。

第二,复杂度适中但值得演示。 它不像简单的计数器那样"一个变量打天下",也不像企业级 App 那样需要全局状态管理。刚刚好能展示 @State 处理数组、嵌套对象的能力。

第三,为后续扩展留足空间。 如果以后需求变成"多页面共享待办数据",只需要把 @State 替换成 @Provide/@Consume 或 AppStorage 方案,架构上完全兼容。

3.3 一些反模式警示

在正式写代码之前,有几个坑一定要提前告诉大家,这些坑在社区里被问了无数次,踩一个能让你头疼一整天:

反模式一:把所有数据都塞进一个 @State 对象。

// ❌ 不推荐:一个大对象管所有状态
@State appData: AppData = new AppData();

// ✅ 推荐:按功能拆分成多个独立的 @State
@State todos: TodoItem[] = [];
@State filter: string = 'all';
@State isLoading: boolean = false;

虽然技术上一个大对象能管所有事,但这样做的问题是——任意一个字段变化,整个大对象都被认为"变了",ArkUI 会触发相关的重渲染,可能导致不必要的性能开销。 把状态按功能拆分,每个 @State 只管自己那一块,粒度更细,性能更好。

反模式二:在 @State 变量里放不相关的状态。

// ❌ 不推荐:把无关状态混在一起
@State userProfile: UserProfile = { name: '', avatar: '' };
@State documentList: Document[] = [];

// ✅ 推荐:只把"影响当前组件 UI 的状态"放进 @State
@State documentList: Document[] = [];

比如"用户头像"和"文档列表"放在同一个组件里吗?一般不会。所以不要把它们硬凑到同一个 @State 对象里。

反模式三:数组和对象原地修改。

这是最重要的一条,必须说三遍:

@State 监控的是引用地址的变化,不是内容的变化!
@State 监控的是引用地址的变化,不是内容的变化!
@State 监控的是引用地址的变化,不是内容的变化!

this.todos.push(newItem) 这种写法在 @State 里不会触发界面更新!后面第五节会详细解释原因。


四、完整代码实现

下面给出精简后的核心代码。为控制篇幅,每个代码块控制在 25~30 行左右,重点展示关键逻辑,完整代码请移步 GitHub 仓库。

4.1 入口组件与状态定义

// EntryComponent.ets
@Entry
@Component
struct EntryComponent {
  // 用 @State 装饰器"升级"数据为受监控状态
  @State todos: TodoItem[] = [
    { id: 1, title: '学习 ArkUI 状态管理', completed: false, priority: 2 },
    { id: 2, title: '完成计数器 Demo', completed: true, priority: 1 },
  ];
  @State inputText: string = '';
  @State filter: string = 'all';

  // 计算属性:根据筛选状态返回不同的列表
  get filteredTodos(): TodoItem[] {
    if (this.filter === 'active') {
      return this.todos.filter((t) => !t.completed);
    } else if (this.filter === 'completed') {
      return this.todos.filter((t) => t.completed);
    }
    return this.todos;
  }

  build() {
    Column() {
      // …… UI 结构,下一小节展示
    }
    .width('100%')
    .height('100%')
    .padding(20);
  }
}

关键点就一个:@State 写在变量前面,这个变量就"活了"——变一下,界面动一下。

4.2 添加新任务

// 在 build() 方法中调用,绑定到输入框和按钮
addTodo() {
  if (this.inputText.trim() === '') {
    return;
  }
  const newTodo: TodoItem = {
    id: Date.now(), // 用时间戳生成唯一ID
    title: this.inputText.trim(),
    completed: false,
    priority: 1,
  };
  // ⭐ 关键:创建新数组赋值,而不是 push
  this.todos = [...this.todos, newTodo];
  this.inputText = ''; // 清空输入框
}

再次强调:this.todos = [...this.todos, newTodo] 是必须的!如果你写成 this.todos.push(newTodo),界面不会刷新。这是新手最容易踩的坑,没有之一。

4.3 切换完成状态

toggleTodo(id: number) {
  // 遍历找到目标项,生成新的数组(完成状态取反)
  this.todos = this.todos.map((todo) =>
    todo.id === id ? { ...todo, completed: !todo.completed } : todo
  );
}

这里用到了数组的 .map() 方法——它返回一个新数组,正好符合 @State 的要求。{ ...todo, completed: !todo.completed } 这行代码用了对象展开语法,功能是把目标对象的 completed 字段取反,其他字段保持不变。

4.4 删除任务

deleteTodo(id: number) {
  this.todos = this.todos.filter((todo) => todo.id !== id);
}

过滤掉指定 ID 的任务,返回新数组赋值给 @State 变量。简洁高效,一行搞定。

4.5 完整 UI 结构

// build() 方法内的核心 UI 片段
Row() {
  TextInput({ placeholder: '输入新任务', text: this.inputText })
    .onChange((value: string) => { this.inputText = value; })
    .width('70%');
  Button('添加')
    .onClick(() => this.addTodo())
    .width('25%');
}
.width('100%')
.margin({ bottom: 16 });

// 待办列表
ForEach(this.filteredTodos, (todo: TodoItem) => {
  Row() {
    Checkbox()
      .select(todo.completed)
      .onChange((value: boolean) => {
        todo.completed = value;
        this.toggleTodo(todo.id);
      });
    Text(todo.title)
      .fontSize(16)
      .fontColor(todo.completed ? '#999' : '#000');
    Blank();
    Button('删除')
      .onClick(() => this.deleteTodo(todo.id));
  }
  .width('100%')
  .padding(12)
  .backgroundColor('#f5f5f5')
  .margin({ bottom: 8 });
});

ArkUI 的 ForEach 是渲染列表的标准方式。它会自动跟踪数组变化——数组变长了,它就多渲染几行;变短了,它就少渲染几行。你不需要手动操作 DOM,不需要调用任何刷新 API,只需要管好数据本身。


五、深度技术原理

5.1 响应式系统的核心思想

要理解 @State 为什么能做到"变量变了界面就跟着变",我们得先弄清楚一个概念——响应式编程

打个比方来解释。想象一下,你的手机收到一条微信消息时,屏幕会亮、通知栏会有提示、聊天列表里会出现新头像。这些动作不是你手动触发的,而是手机系统"感知"到新消息后自动做出的响应。

@State 就是这样一套"感知+响应"机制。它在变量和数据源之间建立了一条隐形的"通知通道":

用户操作 → 修改 @State 变量
         → ArkUI 框架检测到变化
         → 触发 UI 重新渲染
         → 界面更新显示最新数据

整个过程你不需要写一行通知代码,这就是"响应式"的核心含义——变化自动传播,不需要手动干预。

5.2 为什么数组要整体替换而不能原地修改?

这是理解 @State 的最难的部分,也是最容易踩坑的地方。让我把它拆开来讲。

先说计算机里的"值"和"引用":

想象你有一张便利贴,上面写着"会议室A"。这张便利贴就是变量,而"会议室A"是变量指向的房间。

  • 基本类型(数字、字符串)就像直接把内容写在便利贴上——改一次就是换一张新的便利贴。
  • 数组和对象就像便利贴上写的是"黑板编号B",你得去黑板那边看具体内容。黑板上的内容改了,但便利贴上写的信息没变。

ArkUI 框架在检查 @State 变量是否变化时,用的是引用比较——它看的是便利贴上的内容有没有变,而不是黑板上的内容有没有变。

所以:

  • this.todos.push(item) → 改了黑板上的内容,但便利贴没换,框架认为"没变化",界面不刷新
  • this.todos = [...this.todos, item] → 相当于换了一张新便利贴,框架检测到"变了",界面刷新

这就好比你给同事发消息说"会议室地址改了",你得重新发一条消息通知他,而不是自己悄悄去换门牌号——通知系统才能感知到变化。

5.3 重新渲染的边界:最小化更新

ArkUI 的 @State 不是一变化就重新渲染整个页面,它非常聪明,会尽量缩小更新范围。

打个比方:一个待办列表里有100条任务,你只改了第3条的状态。ArkUI 不会傻乎乎地把100条都重新渲染一遍——它只会把第3条那个 Row 组件刷新一下。这就是"最小化更新"策略,性能就是这么来的。

具体是怎么做到的呢?ArkUI 内部维护了一个"组件依赖图"——记录了每个 UI 组件依赖了哪些 @State 变量。当变量变化时,它只通知相关的组件去刷新,不相干的组件完全不知道发生了什么。

可以把这个机制想象成一个"订阅-发布"系统:

@State 变量 → [组件A, 组件C, 组件F] 订阅了它
           → 变量变化 → 只通知 A、C、F 重新渲染
           → 组件 B、D、E 完全不受影响

这就是为什么我们一直强调组件拆分的重要性——把界面拆得越小、越独立,每个组件依赖的状态越少,重渲染的粒度就越细,整体性能就越好。

5.4 @State 的生命周期

@State 变量有两个关键时间点:

初始化阶段: 第一次进入组件时,@State 变量被赋予初始值,UI 根据这个初始值渲染出第一帧画面。

变化检测阶段: 每次变量被重新赋值(注意是重新赋值,不是原地修改),ArkUI 触发"脏检查"——对比新旧值,发现不同就触发更新。

需要特别注意的是:组件被销毁时,@State 的值也会被清除。 如果你需要持久化数据(比如用户关闭页面后再打开,数据还在),需要配合 @StorageLink 或者数据库来使用。@State 本身是"活在组件里的",组件没了,它就没了。

5.5 脏检查与批量更新

ArkUI 内部还有一个重要的优化机制——批量更新

想象一下这个场景:你在一个点击事件里,同时修改了三个 @State 变量:

Button('全选').onClick(() => {
  this.selectAll = true;
  this.selectedCount = this.todos.length;
  this.isAllSelected = true;
});

如果不做优化,框架可能会触发三次 UI 渲染。但 ArkUI 会把这三次修改"合并"成一次,在事件处理完成后统一刷新界面。这就是批量更新机制,它避免了"改一次刷一次"的性能浪费。


六、常见问题解答

Q1:@State 和 @Link 有什么区别?

这是新手最容易混淆的一对概念。

简单说:@State 是"我的数据我自己管",@Link 是"我的数据跟别人同步"。

想象一下在一个公司里:

  • @State 就像你手里自己保管的笔记本,记的是你自己的事情,改了只有你自己知道。
  • @Link 就像你和同事共用的白板,你写一个字,同步到对方那边;对方改一下,也同步到你这边。两个人看到的内容永远是一致的。

具体到代码上:

// 父组件
@State parentCount: number = 0;

// @Link 接收的是父组件的引用,不是副本
ChildComponent({ count: $parentCount })
// 子组件
@Link count: number;

// 子组件里修改 count,会直接影响父组件的 parentCount
this.count++;

所以 @Link 适合父子组件需要双向同步数据的场景,比如表单里的多个输入框互相联动。

使用场景对比:

场景 推荐方案
父组件管理自己的数据,子组件只读 @State + @Prop
父子组件需要双向修改同一个数据 @State + @Link
多层嵌套的父子组件共享数据 @Provide + @Inject
跨页面、跨组件的全局数据 AppStorage

Q2:为什么改了数组/对象的内容,界面不更新?

这个问题在第五节的原理部分已经详细解释过了。核心原因是:@State 检测的是"引用变化",不是"内容变化"。

正确做法:创建新数组/新对象来赋值。

// ❌ 错误
this.list.push(newItem);
this.user.name = '新名字';
this.numbers[0] = 100; // 通过下标修改数组元素

// ✅ 正确
this.list = [...this.list, newItem];
this.user = { ...this.user, name: '新名字' };
this.numbers = this.numbers.map((n, i) => i === 0 ? 100 : n);

这条规则要刻进 DNA 里。ArkUI 里所有的响应式操作都遵循这个原则,不仅仅是 @State@Observed + @ObjectLink 组合、@StorageLink 同样如此。

Q3:@State 能跨页面使用吗?

不能直接跨页面使用。@State 的作用域是组件级别的,超出组件就鞭长莫及。

如果你需要在多个页面之间共享状态,有几种方案:

方案 适用场景 特点
AppStorage 全局应用状态 键值对,持久化
LocalStorage 页面级共享 页面内多组件共享
@Provide/@Inject 祖先后代链 不需要逐层传递
路由传参 页面跳转传值 适合一次性数据

对于"待办事项管理器"这类单页面应用,@State 完全够用。如果是多页面 App,建议配合 AppStorage 来做全局状态管理。

Q4:一个组件里可以定义多个 @State 吗?

当然可以!你想定义多少个就定义多少个。它们各自独立,互不影响。

@State count: number = 0;
@State username: string = '';
@State isLoading: boolean = false;
@State userList: User[] = [];

不过要注意的是:每个 @State 变化都会触发一次重新渲染。 虽然 ArkUI 会尽量合并多次变化(比如在同一个事件回调里连续修改多个 @State,最后只会有一次 UI 刷新),但如果你在 aboutToAppear() 或者定时器里频繁修改多个 @State,还是要注意性能问题。

Q5:@State 和 Vue 的响应式有什么区别?

这是很多从 Vue 转过来的开发者关心的问题。两者的设计思想是一致的(数据变化驱动视图更新),但实现机制有差别。

Vue 2 使用的是 Object.defineProperty(劫持对象的 get/set),Vue 3 和 ArkUI 使用的是 Proxy(代理)。Proxy 的优势是能监听新增属性和数组下标访问,Object.defineProperty 则需要在初始化时就得知道有哪些属性。

最关键的区别还是赋值方式

// ArkUI @State:需要重新赋值整个数组/对象
this.items = [...this.items, newItem];

// Vue:支持数组的 push/pop/splice 原生方法(通过 Proxy 自动追踪)
this.items.push(newItem);

ArkUI 的方式要求开发者写代码时更"刻意"一些,但好处是边界清晰——什么时候触发更新一目了然,不会因为 JavaScript 数组方法的隐晦行为而产生难以追踪的 bug。

反过来说,Vue 的响应式虽然写起来更自然,但有时候会遇到"不知道为什么没更新"的玄学问题(比如直接赋值了一个新对象覆盖,而不是用响应式方法修改)。ArkUI 的规则更明确:想触发更新,就必须重新赋值。 没有例外,没有例外,没有例外。

Q6:@State 变量能作为函数参数传递吗?

可以,但要分情况。

@State message: string = 'Hello';

// ✅ 作为参数传递(传递的是值拷贝)
this.printMessage(this.message);

// ❌ 不要直接传递 this.message 给需要修改的函数
// 如果函数需要修改这个值,用 @Link 而不是传参

原则就一条:如果你需要"改"这个值,用 @Link;只是"读"这个值,普通传参就行。

Q7:数字类型的 @State 需要特殊处理吗?

不需要!数字、字符串、布尔值这些基本类型在 @State 里用起来是最省心的。因为基本类型是"值传递"的,每次赋值都会创建新的值,ArkUI 的引用比较机制天然能检测到变化:

@State count: number = 0;

// ✅ 正常工作,不需要任何特殊处理
this.count++;
this.count = this.count + 1;
this.count = parseInt(this.count.toString()) + 1;

基本类型不会有数组和对象那种"引用不变但内容变了"的问题,所以你可以放心地直接修改值。


七、运行效果

下面用 ASCII 字符画展示这个待办事项管理器的运行界面。实际运行效果以真机/模拟器为准,这里仅供参考。

初始状态:

在这里插入图片描述

添加新任务后的状态:

在这里插入图片描述

切换到"未完成"筛选后的状态:

在这里插入图片描述

操作说明:

  1. 添加任务:在输入框输入任务标题,点击"添加"按钮 → 新任务出现在列表顶部,输入框自动清空
  2. 完成标记:点击任务左侧的圆圈 → 切换完成状态,已完成任务标题变灰并带删除线,左侧变为实心圆圈
  3. 删除任务:点击"删除"按钮 → 从列表中移除该任务,列表自动重新排列
  4. 筛选任务:点击"全部/未完成/已完成"按钮 → 筛选显示不同状态的待办事项
  5. 统计信息:底部实时显示当前未完成任务数量

视觉效果亮点:

  • 未完成任务:黑色文字 + 左侧空心圆圈
  • 已完成任务:灰色文字 + 左侧实心圆圈 + 删除线 + 更淡的背景色
  • 输入框为空时,"添加"按钮点击无效(做了空值保护)

八、扩展方向

8.1 从 @State 到持久化存储

现在的待办事项管理器,数据是存在内存里的。一旦关闭页面或退出应用,数据就消失了。在真实场景里,这个显然是不可接受的。

扩展方向一:接入 AppStorage 持久化。

// 将 @State 改为 @StorageLink,自动同步到本地存储
@StorageLink('todos') todos: TodoItem[] = [];
@StorageLink('inputText') inputText: string = '';

这一行代码的改动,就能让数据在应用重启后自动恢复。AppStorage 是 HarmonyOS 提供的全局键值存储,底层基于 LottieKVStore 实现,兼顾了速度和可靠性。

背后的原理: @StorageLink 在内部创建了一个与 AppStorage 的双向绑定。当你修改 todos 时,ArkUI 不仅会触发 UI 更新,还会把新的值序列化后写入本地存储。下次启动时,自动从存储中读取上次的值,恢复到界面上。你不需要写任何加载和保存的代码,框架全包了。

8.2 从单组件到多组件拆分

现在的代码虽然功能完整,但所有东西都堆在一个组件里,代码行数多了以后会变得难以维护。

扩展方向二:拆分成独立的子组件。

EntryComponent (根组件,管理 @State)
├── HeaderComponent (页面头部)
├── InputSection (输入区)
├── FilterTabs (筛选标签)
├── TodoList (列表容器)
│   ├── TodoItem × N (单个待办项组件)
│   └── EmptyState (空列表提示)
└── FooterSummary (底部统计)

拆分的原则是:每个组件只关心自己那一小块数据和 UI。 TodoItem 组件不需要知道"筛选条件"是什么,它只需要知道"我这个任务是否已完成",以及"被点击时该干什么"。

子组件之间需要通信时,通过 @Link@Prop 传递数据。这样整个应用的架构就清晰了,出了问题也容易定位。

8.3 添加动画过渡效果

ArkUI 内置了显式动画和属性动画能力,可以给状态变化加上流畅的视觉效果。

// 任务完成时加一个缩放动画
Text(this.title)
  .fontColor(this.completed ? '#999' : '#000')
  .animation({
    duration: 300,
    curve: Curve.EaseOut,
  });

// 删除时加一个渐隐效果
Row()
  .transition(TransitionEffect.OPACITY.animation({ duration: 200 }))

好的动画不仅让界面更美观,更重要的是能提供视觉反馈——让用户清楚地知道"我刚才的操作被系统接收了"。这是提升 App 体验感的关键细节。

8.4 支持任务优先级和拖拽排序

在数据模型里我们预留了 priority 字段,可以基于这个字段实现:

  • 优先级排序:高优先级任务排在前面
  • 拖拽排序:长按任务项后拖动到目标位置,改变列表顺序

拖拽排序在 ArkUI 里通过手势事件 gestureonDragStart/onDrop 来实现。这涉及到更复杂的 UI 状态管理——不仅仅是改一个布尔值,而是改整个数组的顺序。@State 完全能 handle 这种情况,你需要做的就是在 onDrop 回调里更新数组顺序,ArkUI 会自动渲染出新的排列效果。

8.5 接入网络请求

如果这是一个真实项目,数据肯定不能只存本地——需要从后端接口拉取任务列表、新增任务时同步到服务器。

扩展方向五:基于 @State + 网络请求的状态管理。

@State isLoading: boolean = false;
@State errorMessage: string = '';
@State todos: TodoItem[] = [];

async loadTodos() {
  this.isLoading = true;
  this.errorMessage = '';
  try {
    const data = await fetchTodosFromServer();
    this.todos = data; // 网络数据赋值给 @State,自动渲染
  } catch (e) {
    this.errorMessage = '加载失败,请检查网络';
  } finally {
    this.isLoading = false;
  }
}

网络请求是一个天然的"异步操作",配合 @State 可以优雅地管理加载中、加载成功、加载失败三种状态的 UI 展示。比如:

  • isLoading = true → 显示加载动画
  • errorMessage != '' → 显示错误提示
  • todos 有数据 → 显示列表

三个状态变量各司其职,界面根据它们的值自动切换到对应的展示状态。

8.6 本地通知提醒

如果产品经理说"待办事项快到期了要给用户发通知",可以在数据模型里加一个 reminderTime 字段,然后用 HarmonyOS 的通知管理能力实现定时提醒。

这个功能的数据流是:用户在编辑页面设置提醒时间 → 存入 @State → 同步到 AppStorage → 通知模块读取 AppStorage 的值 → 定时触发系统通知。 全程不需要手动调用刷新 API,@State 和 AppStorage 的响应式机制自动驱动整个链路。


总结

写到这里,我们来回顾一下今天学到的内容:

@State 是什么? 它是 ArkUI 里的状态装饰器,被它修饰的变量一变,界面自动跟着变。它把"数据"和"视图"之间的同步工作接管了过去,让开发者只专注业务逻辑,不用手动维护视图更新。

怎么用? 记住一个核心口诀——“声明式 + 整体赋值”。声明变量时前面加 @State,修改数组或对象时用展开语法创建新数组/新对象再赋值。这两点做到位,基本就不会踩坑了。

背后的原理是什么? ArkUI 维护了一个组件依赖图,@State 变化时只精准更新受影响的那部分 UI,不是整个页面重建。这就是它性能出色的原因。数组/对象要整体替换的原因是框架用引用比较来检测变化——便利贴上的地址变了才算变化,黑板上的内容变了不算。

还有什么要注意的? @State 只管组件内的状态,跨组件用 @Link/@Provide,持久化用 AppStorage。方案组合使用,不要一个 @State 打天下。

好了,关于 @State 的内容就讲到这里。建议大家把文章配套的代码克隆下来,跑一跑、改一改。改数字、加字段、删功能、接接口、加上动画——动手折腾才是最好的学习方式。 有任何问题欢迎在 GitHub 上提 Issue,我们一起探讨。


Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐