在上一篇基础开发教程的基础上,本次聚焦鸿蒙原生开发的核心核心 —— 状态变量管理(ArkTS 响应式核心),这是实现 “状态驱动 UI” 的关键,也是开发中复杂页面、跨组件交互的基础。本文会从基础状态装饰器、跨组件状态共享、复杂类型状态管理、实战场景落地四个维度,结合 HarmonyOS App 开发实操,让你彻底掌握鸿蒙状态管理的用法,同时实现一个带跨页面状态共享、复杂数据渲染、子组件状态联动的实战小应用,将状态管理知识落地到实际开发中。

一、ArkTS 状态管理核心认知

1.1 状态管理的本质

ArkTS 的状态管理是响应式编程在鸿蒙中的实现,核心逻辑是:状态变量被装饰器标记后,其值的变化会被框架监听,当值发生改变时,框架会自动重新执行依赖该变量的组件 build() 方法,实现 UI 刷新

区别于传统命令式开发(手动操作 DOM / 组件更新),声明式开发中开发者只需关注状态的变化,无需关心 UI 如何刷新,这也是 ArkUI、Flutter、Compose 等现代 UI 框架的核心优势。

1.2 鸿蒙状态管理的核心分类

根据状态变量的作用域、数据流向、使用场景,ArkTS 将核心状态装饰器分为三大类,覆盖单个组件、父子组件、跨组件 / 跨页面所有状态共享场景,新手先掌握核心装饰器,进阶再学习扩展能力:

装饰器 作用域 数据流向 核心用途 初始化要求
@State 单个组件(自身) 自身修改自身用 组件内部的状态变量 组件内部初始化
@Prop 子组件 父→子(单向) 父组件向子组件传递基础类型值 必须由父组件传入,不可自初始化
@Link 子组件 父↔子(双向) 父子组件双向同步状态 必须由父组件传入(绑定 @State/@Link 等)
@Provide 祖先组件 祖→孙(多向) 跨层级组件状态共享(无需逐层传递) 祖先组件初始化
@Consume 后代组件 祖↔孙(双向) 接收 @Provide 的状态变量 无需初始化,与 @Provide 同名绑定
@StorageLink 全局(整个应用) 全局双向同步 跨页面、跨组件全局状态共享 全局初始化(可选)

1.3 核心原则(避坑关键)

  1. 状态变量只能在 UI 主线程修改:鸿蒙框架仅监听 UI 线程的状态变化,子线程修改需通过 UIContext 切回主线程;
  2. 基础类型 vs 复杂类型number/string/boolean 等基础类型可直接修改值触发刷新;Object/Array/自定义类 等复杂类型,直接修改子属性不会触发刷新,需通过重新赋值深度监听装饰器实现;
  3. 装饰器不可滥用:按作用域选择最小粒度的装饰器(如仅组件内部使用用@State,而非全局的@StorageLink),减少框架监听开销;
  4. 状态变量尽量私有化:标记为 private(如@State private count: number = 0),避免外部直接修改,通过组件方法统一管理状态变化(符合封装原则)。

二、基础状态装饰器实战(@State + @Prop + @Link)

这三个装饰器是父子组件状态交互的核心,也是所有状态管理的基础,先通过「父组件传值给子组件、子组件修改父组件状态」的实战场景,掌握其用法和区别。

2.1 实战场景设计

实现一个计数器父子组件联动页面:

  • 父组件:包含总计数器、一个子组件(增量器)、一个子组件(减量器);
  • 增量器子组件:通过@Link双向绑定父组件计数器,点击实现 + 1;
  • 减量器子组件:通过@Link双向绑定父组件计数器,点击实现 - 1;
  • 额外实现一个只读展示子组件:通过@Prop单向接收父组件计数器,仅展示不修改。

2.2 步骤 1:创建子组件(封装到单独文件,符合组件化规范)

entry/src/main/ets/pages 目录下新建 components 文件夹(用于存放自定义子组件),分别创建 3 个子组件文件:

子组件 1:增量器(CounterAdd.ets)- @Link 双向绑定
// 增量器子组件:双向修改父组件计数器
import { Button, ButtonType } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';

// 子组件无需@Entry,仅用@Component标记
@Component
export struct CounterAdd {
  // 用@Link双向绑定父组件的状态变量,必须由父组件传入,不可自初始化
  @Link count: number;

  build() {
    Button('点击+1', { type: ButtonType.Capsule })
      .width(180)
      .height(50)
      .fontSize(18)
      .backgroundColor('#007AFF')
      .onClick(() => {
        // 直接修改@Link变量,会同步到父组件的@State变量,触发父子组件UI同时刷新
        this.count++;
      });
  }
}
子组件 2:减量器(CounterMinus.ets)- @Link 双向绑定
// 减量器子组件:双向修改父组件计数器
import { Button, ButtonType } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';

@Component
export struct CounterMinus {
  // 与父组件的count双向绑定
  @Link count: number;

  build() {
    Button('点击-1', { type: ButtonType.Capsule })
      .width(180)
      .height(50)
      .fontSize(18)
      .backgroundColor('#FF3B30')
      .onClick(() => {
        if (this.count > 0) {
          this.count--;
        }
      });
  }
}
子组件 3:只读展示(CounterShow.ets)- @Prop 单向接收
// 只读展示子组件:单向接收父组件计数器,不可修改
import { Text } from '@ohos/ui.components';
import { FontWeight } from '@ohos/ui.text';

@Component
export struct CounterShow {
  // 用@Prop单向接收父组件值,父组件修改会同步到子组件,子组件无法修改父组件
  @Prop count: number;
  // 子组件自身的私有状态(@State),仅自身使用
  @State title: string = '当前计数器值:';

  build() {
    Text(`${this.title}${this.count}`)
      .fontSize(28)
      .fontWeight(FontWeight.Bold)
      .margin({ top: 20, bottom: 20 });
  }
}

2.3 步骤 2:父组件整合子组件(index.ets)- @State 作为核心状态

修改首页 index.ets,引入上述 3 个子组件,用@State定义核心计数器,实现父子组件状态联动:

// 父组件:首页,核心状态管理
import { Column, Flex, FlexAlign } from '@ohos/ui.components';
// 引入自定义子组件(注意路径正确)
import { CounterAdd } from './components/CounterAdd';
import { CounterMinus } from './components/CounterMinus';
import { CounterShow } from './components/CounterShow';

// 页面入口组件
@Entry
@Component
struct StateManagePage {
  // 父组件核心状态变量:@State标记,组件内部初始化
  @State private totalCount: number = 0;

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Center
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        // 子组件1:只读展示,@Prop单向传值 → 直接传变量即可
        CounterShow({ count: this.totalCount });

        // 弹性布局:放+1和-1按钮
        Flex({
          align: FlexAlign.Center,
          justifyContent: FlexAlign.SpaceAround
        })
          .width('100%')
          .margin({ bottom: 20 }) {
            // 子组件2:增量器,@Link双向绑定 → 传值时加$符号(关键!)
            CounterAdd({ count: $totalCount });
            // 子组件3:减量器,@Link双向绑定 → 传值时加$符号
            CounterMinus({ count: $totalCount });
          }
      }
  }
}

2.4 关键知识点解析

  1. @Link 传值必须加 $ 符号:父组件向子组件传递@Link变量时,需用$变量名(如$totalCount),表示传递变量的引用,而非单纯的值,这是双向绑定的核心;
  2. @Prop 传值直接传变量@Prop是单向值传递,直接传this.totalCount即可,子组件修改该变量不会同步到父组件(若子组件尝试修改,编译器会报错);
  3. 子组件无需 @Entry:只有页面入口组件需要@Entry标记,自定义子组件仅需@Component,可被其他组件无限引用;
  4. 状态变化的联动性:点击 + 1/-1 按钮时,子组件修改@Link变量,会直接同步到父组件的@State变量,框架会自动刷新所有依赖该变量的组件(父组件的总计数器、子组件的展示文本),无需手动操作。

2.5 运行效果

启动应用后,点击「点击 + 1」/「点击 - 1」,中间的计数器数值会实时变化,且所有组件的数值保持一致,实现了父子组件的状态双向联动单向传值展示

三、跨层级状态管理(@Provide + @Consume)

在实际开发中,页面结构往往是多层嵌套的(如「父组件→子组件→孙组件」),如果用@Prop/@Link实现跨层级传值,需要逐层传递(父传子、子传孙),代码冗余且维护性差。

鸿蒙提供的@Provide + @Consume装饰器,实现了祖先组件到后代组件的跨层级状态共享,无需逐层传值,只需在祖先组件用@Provide标记状态,后代组件用@Consume同名绑定即可,支持双向同步

3.1 实战场景设计

实现三层组件嵌套的状态共享:

  • 祖先组件(根页面):用@Provide定义主题色状态themeColor: string),包含一个修改主题色的按钮;
  • 子组件:仅做布局嵌套,不处理状态;
  • 孙组件:用@Consume绑定祖先组件的主题色,展示一个随主题色变化的色块和文本。

3.2 步骤 1:创建子组件和孙组件

components文件夹下创建:

孙组件(ThemeBlock.ets)- @Consume 绑定状态
// 孙组件:跨层级接收祖先组件的主题色
import { Column, Text, Stack } from '@ohos/ui.components';
import { FontWeight } from '@ohos/ui.text';
import { FlexAlign } from '@ohos/ui.components.common';

@Component
export struct ThemeBlock {
  // 用@Consume同名绑定祖先组件的@Provide变量,无需初始化、无需父组件传值
  @Consume themeColor: string;

  build() {
    Column({ align: FlexAlign.Center }) {
      Text('孙组件:跨层级共享主题色')
        .fontSize(20)
        .margin({ bottom: 10 });
      // 随主题色变化的色块
      Stack()
        .width(200)
        .height(200)
        .backgroundColor(this.themeColor)
        .borderRadius(10);
    }
  }
}
子组件(ThemeChild.ets)- 仅嵌套孙组件
// 子组件:仅做布局嵌套,不处理状态
import { Column } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';
// 引入孙组件
import { ThemeBlock } from './ThemeBlock';

@Component
export struct ThemeChild {
  build() {
    Column({ align: FlexAlign.Center })
      .width('100%')
      .margin({ top: 30 }) {
        // 直接引入孙组件,无需传递主题色
        ThemeBlock();
      }
  }
}

3.3 步骤 2:祖先组件(根页面)- @Provide 定义状态

新建页面ProvideConsumePage.ets(在pages目录下),作为祖先组件,定义@Provide状态,整合子组件:

// 祖先组件:跨层级状态管理核心
import { Column, Button, ButtonType, Text } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';
import { FontWeight } from '@ohos/ui.text';
// 引入子组件
import { ThemeChild } from './components/ThemeChild';

@Entry
@Component
struct ProvideConsumePage {
  // 用@Provide标记跨层级共享的状态,祖先组件初始化
  @Provide private themeColor: string = '#007AFF'; // 初始蓝色
  // 主题色列表
  private colorList: string[] = ['#007AFF', '#34C759', '#FF9500', '#FF3B30', '#AF52DE'];

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Center
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        Text('祖先组件:定义主题色')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 20 });

        // 修改主题色按钮
        Button('切换主题色', { type: ButtonType.Capsule })
          .width(200)
          .height(50)
          .fontSize(18)
          .backgroundColor(this.themeColor)
          .fontColor('#FFFFFF')
          .onClick(() => {
            // 随机切换主题色,修改@Provide变量
            const randomIndex = Math.floor(Math.random() * this.colorList.length);
            this.themeColor = this.colorList[randomIndex];
          });

        // 子组件:仅嵌套,无需传值
        ThemeChild();
      }
  }
}

3.4 关键知识点解析

  1. 同名绑定原则@Provide@Consume的变量名必须完全一致(如都为themeColor),框架通过变量名实现自动绑定,无需手动关联;
  2. 跨层级无限制:无论嵌套多少层(父→子→孙→重孙),只要后代组件用@Consume同名标记,就能直接获取祖先组件的@Provide状态;
  3. 双向同步:后代组件修改@Consume变量,会同步到祖先组件的@Provide变量,反之亦然,实现跨层级双向联动;
  4. 作用域限制@Consume只能绑定最近的祖先组件@Provide状态(若多个祖先组件有同名@Provide,优先绑定最近的一个)。

3.5 运行效果

点击「切换主题色」按钮,祖先组件的按钮背景色和孙组件的色块背景色会同步随机变化,中间的子组件无需做任何状态处理,实现了跨层级的状态共享。

四、全局状态管理(@StorageLink)

在鸿蒙应用开发中,跨页面状态共享(如「首页→个人中心→设置页」共享用户信息、主题设置、登录状态)是常见需求,@State/@Provide等装饰器的作用域仅限于组件树内部,无法实现跨页面共享。

鸿蒙提供了全局状态存储(AppStorage),结合@StorageLink装饰器,可实现整个应用的全局状态共享,所有页面 / 组件都能绑定并修改全局状态,数据实时同步。

4.1 核心概念:AppStorage

AppStorage是鸿蒙框架提供的全局单例存储容器,生命周期与应用一致(应用启动时创建,应用退出时销毁),用于存储全局共享的状态数据。

  • @StorageLink:将组件的状态变量与AppStorage中的指定键值双向绑定,组件修改变量会同步到AppStorageAppStorage中的值变化会同步到所有绑定的组件;
  • 补充:@StorageProp:单向绑定,组件只能读取AppStorage的值,不能修改(适合全局只读状态,如应用版本、全局配置)。

4.2 实战场景设计

实现跨页面全局计数器

  1. 全局状态:在AppStorage中定义全局计数器globalCount,初始值为 0;
  2. 页面 1(GlobalPage1.ets):绑定全局计数器,实现 + 1 操作,展示当前值;
  3. 页面 2(GlobalPage2.ets):绑定全局计数器,实现 - 1 操作,展示当前值;
  4. 路由跳转:两个页面互相跳转,跳转后计数器的全局状态保持一致,不会重置。

4.3 步骤 1:配置路由(实现页面跳转)

修改之前的router_config.ts(在ets目录下),添加两个全局状态页面的路由配置:

// 全局路由配置
export default {
  routes: [
    { path: '/', name: 'StateManage', page: 'pages/index' }, // 基础状态管理页
    { path: '/provide', name: 'ProvideConsume', page: 'pages/ProvideConsumePage' }, // 跨层级状态页
    { path: '/global1', name: 'GlobalPage1', page: 'pages/GlobalPage1' }, // 全局状态页1
    { path: '/global2', name: 'GlobalPage2', page: 'pages/GlobalPage2' } // 全局状态页2
  ]
};

4.4 步骤 2:实现全局状态页面 1(GlobalPage1.ets)- +1 操作

// 全局状态页1:实现+1,绑定全局计数器
import { Column, Text, Button, ButtonType } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';
import { FontWeight } from '@ohos/ui.text';
import router from '@ohos.router';

@Entry
@Component
struct GlobalPage1 {
  // 用@StorageLink绑定AppStorage中的globalCount键,双向同步
  // 若AppStorage中无该键,会自动初始化并添加,初始值为0
  @StorageLink('globalCount') private globalCount: number = 0;

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Center
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        Text('全局状态页1')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 20 });

        Text(`全局计数器:${this.globalCount}`)
          .fontSize(28)
          .margin({ bottom: 30 });

        // 全局计数器+1
        Button('全局+1', { type: ButtonType.Capsule })
          .width(200)
          .height(50)
          .fontSize(18)
          .backgroundColor('#007AFF')
          .fontColor('#FFFFFF')
          .onClick(() => {
            this.globalCount++;
          })
          .margin({ bottom: 15 });

        // 跳转到全局状态页2
        Button('跳转到页2', { type: ButtonType.Capsule })
          .width(200)
          .height(50)
          .fontSize(18)
          .backgroundColor('#34C759')
          .fontColor('#FFFFFF')
          .onClick(() => {
            router.push({
              url: '/pages/GlobalPage2'
            });
          });
      }
  }
}

4.5 步骤 3:实现全局状态页面 2(GlobalPage2.ets)- -1 操作

// 全局状态页2:实现-1,绑定同一个全局计数器
import { Column, Text, Button, ButtonType } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';
import { FontWeight } from '@ohos/ui.text';
import router from '@ohos.router';

@Entry
@Component
struct GlobalPage2 {
  // 绑定AppStorage中的同一个键globalCount,双向同步
  @StorageLink('globalCount') private globalCount: number = 0;

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Center
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        Text('全局状态页2')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 20 });

        Text(`全局计数器:${this.globalCount}`)
          .fontSize(28)
          .margin({ bottom: 30 });

        // 全局计数器-1
        Button('全局-1', { type: ButtonType.Capsule })
          .width(200)
          .height(50)
          .fontSize(18)
          .backgroundColor('#FF3B30')
          .fontColor('#FFFFFF')
          .onClick(() => {
            if (this.globalCount > 0) {
              this.globalCount--;
            }
          })
          .margin({ bottom: 15 });

        // 返回页1
        Button('返回页1', { type: ButtonType.Capsule })
          .width(200)
          .height(50)
          .fontSize(18)
          .backgroundColor('#FF9500')
          .fontColor('#FFFFFF')
          .onClick(() => {
            router.back();
          });
      }
  }
}

4.6 关键知识点解析

  1. 键值绑定原则@StorageLink('键名')通过键名绑定AppStorage中的全局数据,多个页面绑定同一个键名,即可实现状态共享;
  2. 自动初始化:若AppStorage中无指定键名,@StorageLink会将组件的初始值(如0)自动添加到AppStorage中;若已有该键名,组件的初始值会被AppStorage中的值覆盖;
  3. 全局双向同步:任意页面修改@StorageLink变量,会同步到AppStorage,所有绑定该键名的页面 / 组件会自动刷新 UI,实现全局状态联动;
  4. 数据持久化AppStorage内存级存储,应用退出后数据会丢失;若需要持久化全局状态(如登录状态、用户信息),可结合鸿蒙的Preferences(轻量存储)实现「内存 + 本地」双重存储;
  5. @StorageProp 用法:与@StorageLink语法一致(@StorageProp('globalCount')),但仅能读取全局状态,不能修改,适合展示类全局数据。

4.7 运行效果

  1. 在页 1 点击「全局 + 1」,计数器数值增加,跳转到页 2 后,计数器数值与页 1 保持一致;
  2. 在页 2 点击「全局 - 1」,计数器数值减少,返回页 1 后,数值同步更新;
  3. 两个页面的计数器状态全局共享,不会因页面跳转而重置。

五、复杂类型状态管理(Object/Array)

前面的示例都是基础类型(number/string)的状态管理,实际开发中,我们经常需要处理复杂类型(Object:用户信息、商品信息;Array:列表数据、菜单数据)的状态。

由于 ArkTS 的状态监听仅监听变量的引用 / 值变化直接修改复杂类型的子属性 / 内部值,不会触发 UI 刷新,这是开发中最常见的坑,本节重点讲解复杂类型的正确管理方式。

5.1 核心解决方案

针对复杂类型的状态管理,鸿蒙提供3 种核心解决方案,按易用性排序,新手优先掌握前两种:

方案 1:重新赋值(推荐,新手首选)

对 Object/Array 进行浅拷贝重新赋值,修改引用地址,触发框架的状态监听,实现 UI 刷新。

  • Object:使用扩展运算符...)浅拷贝,修改后重新赋值;
  • Array:使用扩展运算符...)、filtermap等方法,返回新数组后重新赋值(避免使用push/pop/shift/unshift等直接修改原数组的方法)。
方案 2:@Observed + @ObjectLink(自定义类深度监听)

若使用自定义类(如UserGoods),可通过@Observed标记类,@ObjectLink标记子组件的状态变量,实现对类子属性的深度监听,直接修改子属性即可触发刷新。

方案 3:使用 Ref 包裹(进阶)

Ref包裹复杂类型,通过value属性修改内部值,框架会监听value的变化(适合嵌套层级极深的复杂数据)。

5.2 实战 1:Object 类型(用户信息)- 重新赋值

实现用户信息修改页面,用@State标记用户对象,通过重新赋值修改子属性,触发 UI 刷新:

// 复杂类型:Object 管理
import { Column, Text, Button, ButtonType } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';
import { FontWeight } from '@ohos/ui.text';

@Entry
@Component
struct ObjectStatePage {
  // 复杂类型:用户信息对象
  @State private userInfo: {
    name: string;
    age: number;
    gender: string;
    phone: string;
  } = {
    name: '鸿蒙开发者',
    age: 25,
    gender: '男',
    phone: '13800138000'
  };

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Center
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        Text('用户信息展示')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 30 });

        // 展示用户信息
        Text(`姓名:${this.userInfo.name}`)
          .fontSize(22)
          .margin({ bottom: 10 });
        Text(`年龄:${this.userInfo.age}岁`)
          .fontSize(22)
          .margin({ bottom: 10 });
        Text(`性别:${this.userInfo.gender}`)
          .fontSize(22)
          .margin({ bottom: 10 });

        // 修改用户信息:重新赋值(关键)
        Button('修改年龄和姓名', { type: ButtonType.Capsule })
          .width(250)
          .height(50)
          .fontSize(18)
          .backgroundColor('#007AFF')
          .fontColor('#FFFFFF')
          .onClick(() => {
            // 错误写法:直接修改子属性,不会触发UI刷新
            // this.userInfo.age = 30;
            // this.userInfo.name = '鸿蒙高级开发者';

            // 正确写法:扩展运算符浅拷贝,重新赋值,修改引用地址
            this.userInfo = {
              ...this.userInfo, // 保留原有属性
              name: '鸿蒙高级开发者', // 修改需要更新的属性
              age: 30
            };
          });
      }
  }
}

5.3 实战 2:Array 类型(列表数据)- 重新赋值

实现列表数据增删页面,用@State标记数组,通过重新赋值实现增删,触发列表 UI 刷新:

// 复杂类型:Array 管理
import { Column, Text, Button, List, ListItem } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';
import { FontWeight } from '@ohos/ui.text';

@Entry
@Component
struct ArrayStatePage {
  // 复杂类型:列表数组
  @State private fruitList: string[] = ['苹果', '香蕉', '橙子', '葡萄'];

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Start
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        Text('水果列表')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 20 });

        // 列表渲染
        List()
          .width('100%')
          .height(300)
          .margin({ bottom: 20 }) {
            ForEach(
              this.fruitList, // 遍历的数组
              (item: string) => { // 遍历项
                ListItem() {
                  Text(item)
                    .fontSize(22)
                    .padding(15);
                }
              },
              (item: string) => item // 唯一标识(必须,否则列表刷新异常)
            );
          }

        // 按钮组
        Column({ align: FlexAlign.Center }) {
          // 添加元素:扩展运算符重新赋值
          Button('添加榴莲', { type: ButtonType.Capsule })
            .width(200)
            .height(45)
            .fontSize(16)
            .backgroundColor('#34C759')
            .fontColor('#FFFFFF')
            .margin({ bottom: 10 })
            .onClick(() => {
              // 错误写法:push直接修改原数组,不触发刷新
              // this.fruitList.push('榴莲');

              // 正确写法:扩展运算符生成新数组,重新赋值
              this.fruitList = [...this.fruitList, '榴莲'];
            });

          // 删除第一个元素:filter返回新数组
          Button('删除第一个元素', { type: ButtonType.Capsule })
            .width(200)
            .height(45)
            .fontSize(16)
            .backgroundColor('#FF3B30')
            .fontColor('#FFFFFF')
            .onClick(() => {
              if (this.fruitList.length > 0) {
                // 正确写法:filter返回新数组,重新赋值
                this.fruitList = this.fruitList.filter((_, index) => index !== 0);
              }
            });
        }
      }
  }
}

5.4 实战 3:自定义类(@Observed + @ObjectLink)- 深度监听

实现自定义用户类的深度监听,直接修改类的子属性即可触发 UI 刷新,无需重新赋值:

步骤 1:定义自定义类,用 @Observed 标记

ets目录下新建model文件夹,创建UserModel.ets

// 自定义用户类,用@Observed标记,实现子属性深度监听
@Observed
export class User {
  name: string;
  age: number;
  address: string;

  constructor(name: string, age: number, address: string) {
    this.name = name;
    this.age = age;
    this.address = address;
  }
}
步骤 2:父组件 + 子组件实现深度监听
// 自定义类:@Observed + @ObjectLink 深度监听
import { Column, Text, Button, ButtonType } from '@ohos/ui.components';
import { FlexAlign } from '@ohos/ui.components.common';
import { FontWeight } from '@ohos/ui.text';
// 引入自定义User类
import { User } from '../model/UserModel';

// 子组件:用@ObjectLink接收自定义类,实现深度监听
@Component
struct UserCard {
  // 用@ObjectLink标记自定义类,必须由父组件传入(加$)
  @ObjectLink user: User;

  build() {
    Column({ align: FlexAlign.Center })
      .width('90%')
      .backgroundColor('#F5F5F5')
      .padding(20)
      .borderRadius(10) {
        Text('用户卡片(子组件)')
          .fontSize(20)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 15 });
        Text(`姓名:${this.user.name}`)
          .fontSize(20)
          .margin({ bottom: 8 });
        Text(`年龄:${this.user.age}岁`)
          .fontSize(20)
          .margin({ bottom: 8 });
        Text(`地址:${this.user.address}`)
          .fontSize(20);

        // 子组件直接修改类的子属性,触发父子组件UI刷新
        Button('子组件修改年龄', { type: ButtonType.Capsule })
          .width(180)
          .height(45)
          .fontSize(16)
          .margin({ top: 15 })
          .onClick(() => {
            this.user.age += 1;
          });
      }
}

// 父组件:页面入口
@Entry
@Component
struct ObservedObjectPage {
  // 父组件用@State标记自定义类实例
  @State private user: User = new User('张三', 28, '北京市海淀区');

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Center
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        Text('自定义类深度监听(父组件)')
          .fontSize(30)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 30 });

        // 子组件:传递自定义类时加$,与@ObjectLink绑定
        UserCard({ user: $user });

        // 父组件直接修改类的子属性,触发父子组件UI刷新
        Button('父组件修改地址', { type: ButtonType.Capsule })
          .width(200)
          .height(50)
          .fontSize(18)
          .backgroundColor('#007AFF')
          .fontColor('#FFFFFF')
          .margin({ top: 30 })
          .onClick(() => {
            this.user.address = '上海市浦东新区';
          });
      }
  }
}

5.5 关键知识点解析

  1. 重新赋值的核心:通过扩展运算符、filtermap等方法,生成新的 Object/Array,修改状态变量的引用地址,让框架监听到状态变化;
  2. ForEach 唯一标识:遍历数组渲染列表时,ForEach的第三个参数必须传递唯一标识(如 item 本身、index+item),否则框架无法准确识别列表项的变化,导致 UI 刷新异常;
  3. @Observed 仅标记类@Observed只能修饰自定义类,不能修饰 Object/Array,且必须与@ObjectLink配合使用;
  4. @ObjectLink 仅用在子组件@ObjectLink只能标记子组件的状态变量,必须由父组件传入(加 $),不可自初始化,实现父子组件对自定义类的深度双向监听

六、状态管理实战整合:打造一个综合型页面

结合以上所有状态管理知识,打造一个综合型实战页面,包含:

  • 组件内部状态(@State);
  • 父子组件双向联动(@State + @Link);
  • 跨层级状态共享(@Provide + @Consume);
  • 复杂类型(Object/Array)管理;
  • 所有状态变化均能正确触发 UI 刷新。

6.1 完整代码(整合页:ComprehensiveStatePage.ets)

// 状态管理综合实战页:整合所有核心装饰器
import { Column, Text, Button, List, ListItem, Flex, Stack } from '@ohos/ui.components';
import { FlexAlign, ButtonType } from '@ohos/ui.components.common';
import { FontWeight } from '@ohos/ui.text';

// 子组件1:商品数量修改(@Link 双向绑定)
@Component
struct GoodsCount {
  @Link count: number;
  @Consume themeColor: string; // 跨层级接收主题色(@Consume)

  build() {
    Flex({ align: FlexAlign.Center, justifyContent: FlexAlign.Center }) {
      Button('-', { type: ButtonType.Circle })
        .width(40)
        .height(40)
        .fontSize(20)
        .backgroundColor(this.themeColor)
        .fontColor('#FFFFFF')
        .onClick(() => {
          if (this.count > 1) this.count--;
        });

      Text(`${this.count}`)
        .fontSize(20)
        .width(40)
        .textAlign('center')
        .margin({ left: 15, right: 15 });

      Button('+', { type: ButtonType.Circle })
        .width(40)
        .height(40)
        .fontSize(20)
        .backgroundColor(this.themeColor)
        .fontColor('#FFFFFF')
        .onClick(() => {
          this.count++;
        });
    }
  }
}

// 子组件2:仅嵌套(跨层级中间层)
@Component
struct GoodsCardWrapper {
  @Link count: number;

  build() {
    Column({ align: FlexAlign.Center })
      .width('90%')
      .margin({ top: 20 }) {
        // 孙组件:直接使用@Consume,无需传值
        GoodsCount({ count: $count });
      }
  }
}

// 主页面入口
@Entry
@Component
struct ComprehensiveStatePage {
  // 组件内部状态(@State)
  @State private goodsName: string = '鸿蒙原生开发实战教程';
  @State private buyCount: number = 1;
  @State private price: number = 99;
  // 复杂类型:商品信息对象(@State)
  @State private goodsInfo: {
    author: string;
    publishTime: string;
    sales: number;
  } = {
    author: '鸿蒙开发者',
    publishTime: '2025-01-01',
    sales: 10000
  };
  // 复杂类型:推荐商品数组(@State)
  @State private recommendList: string[] = ['ArkTS语法详解', 'ArkUI实战', '鸿蒙多端适配'];
  // 跨层级状态(@Provide)
  @Provide private themeColor: string = '#007AFF';
  // 主题色列表
  private colorList: string[] = ['#007AFF', '#34C759', '#FF9500', '#AF52DE'];

  // 计算属性:总价格(基于buyCount和price,无需标记装饰器,依赖状态自动刷新)
  private get totalPrice(): number {
    return this.buyCount * this.price;
  }

  build() {
    Column({
      align: FlexAlign.Center,
      justifyContent: FlexAlign.Start
    })
      .width('100%')
      .height('100%')
      .padding(20) {
        // 标题
        Text('鸿蒙商品详情页(状态管理综合)')
          .fontSize(28)
          .fontWeight(FontWeight.Bold)
          .margin({ bottom: 20 });

        // 切换主题色(修改@Provide)
        Button('切换主题色', { type: ButtonType.Capsule })
          .width(200)
          .height(45)
          .backgroundColor(this.themeColor)
          .fontColor('#FFFFFF')
          .onClick(() => {
            const randomIndex = Math.floor(Math.random() * this.colorList.length);
            this.themeColor = this.colorList[randomIndex];
          })
          .margin({ bottom: 20 });

        // 商品基本信息(复杂类型Object)
        Stack()
          .width('100%')
          .backgroundColor('#F5F5F5')
          .padding(20)
          .borderRadius(10)
          .margin({ bottom: 20 }) {
            Column({ align: FlexAlign.Start }) {
              Text(this.goodsName)
                .fontSize(24)
                .fontWeight(FontWeight.Bold)
                .margin({ bottom: 10 });
              Text(`作者:${this.goodsInfo.author}`)
                .fontSize(18)
                .margin({ bottom: 5 });
              Text(`发布时间:${this.goodsInfo.publishTime}`)
                .fontSize(18)
                .margin({ bottom: 5 });
              Text(`销量:${this.goodsInfo.sales}册`)
                .fontSize(18);
            }
          }

        // 价格+购买数量(父子组件@Link + 跨层级@Consume)
        Flex({ align: FlexAlign.Center, justifyContent: FlexAlign.SpaceBetween })
          .width('100%')
          .margin({ bottom: 20 }) {
            Text(`单价:¥${this.price}`)
              .fontSize(24)
              .fontColor('#FF3B30')
              .fontWeight(FontWeight.Bold);
            Text(`总价:¥${this.totalPrice}`)
              .fontSize(24)
              .fontColor('#FF3B30')
              .fontWeight(FontWeight.Bold);
          }
        // 子组件嵌套:父→子→孙(@Link + @Consume)
        GoodsCardWrapper({ count: $buyCount });

        // 推荐商品列表(复杂类型Array)
        Text('推荐商品')
          .fontSize(22)
          .fontWeight(FontWeight.Bold)
          .alignSelf('flex-start')
          .margin({ bottom: 10 });
        List()
          .width('100%')
          .height(200) {
            ForEach(
              this.recommendList,
              (item) => {
                ListItem() {
                  Text(item)
                    .fontSize(20)
                    .padding(15);
                }
              },
              (item) => item
            );
          }

        // 添加推荐商品(复杂类型Array重新赋值)
        Button('添加推荐商品', { type: ButtonType.Capsule })
          .width(200)
          .height(45)
          .backgroundColor('#34C759')
          .fontColor('#FFFFFF')
          .onClick(() => {
            this.recommendList = [...this.recommendList, `鸿蒙进阶${this.recommendList.length + 1}`];
          });
      }
  }
}

6.2 运行效果

该页面整合了所有状态管理核心能力,所有操作均能正确触发 UI 刷新:

  1. 点击「切换主题色」,按钮背景色和数量修改按钮的背景色跨层级同步变化
  2. 点击数量的「+/-」,购买数量和总价实时联动变化
  3. 点击「添加推荐商品」,推荐商品列表实时新增项
  4. 所有复杂类型(Object/Array)的修改均能正确触发 UI 刷新,无任何卡顿或不刷新问题。

七、状态管理开发规范与避坑指南

7.1 核心开发规范

  1. 装饰器粒度最小化:按作用域选择最合适的装饰器,避免用全局装饰器(@StorageLink)管理组件内部状态;
  2. 状态变量私有化:所有状态变量均标记为private,通过组件方法统一管理状态变化(如updateCount()),避免外部直接修改;
  3. 复杂类型统一管理:对 Object/Array 的修改封装为专用方法,避免在多处直接重新赋值,提高代码可维护性;
  4. 计算属性替代重复逻辑:对于基于状态变量的计算逻辑(如总价格、列表长度),使用计算属性get totalPrice()),无需标记装饰器,依赖的状态变化会自动触发计算;
  5. 组件化拆分:将复杂页面拆分为多个独立子组件,每个子组件仅管理自身的状态,通过装饰器实现组件间的状态交互,符合 “单一职责原则”。

7.2 常见避坑点

  1. 直接修改复杂类型子属性:忘记重新赋值,导致 UI 不刷新,解决方案:扩展运算符浅拷贝重新赋值;
  2. **@Link 传值忘记加:编译器报错,解决方案:父组件向子组件传递变量时,必须加符号;
  3. @Provide/@Consume 变量名不一致:跨层级状态绑定失败,解决方案:保证变量名完全一致;
  4. ForEach 缺少唯一标识:列表刷新异常、重复渲染,解决方案:传递第三个参数作为唯一标识;
  5. 子线程修改状态变量:UI 不刷新甚至应用闪退,解决方案:通过UIContext切回 UI 主线程修改;
  6. 滥用 @StorageLink:所有状态都用全局管理,导致应用性能下降,解决方案:仅跨页面共享的状态使用 @StorageLink;
  7. 自定义类忘记加 @Observed:直接修改子属性不触发刷新,解决方案:用 @Observed 标记自定义类,配合 @ObjectLink 使用。

八、后续进阶学习方向

掌握本次的核心状态管理知识后,你已具备开发中大型鸿蒙原生应用的基础,后续可围绕以下方向进阶:

  1. 高级状态管理:学习LocalStorage(页面级全局存储)、AppStoragePreferences结合实现持久化全局状态
  2. 状态管理框架:学习鸿蒙生态的第三方状态管理框架(如OhReduxArkMobx),适配复杂应用的状态管理;
  3. 生命周期与状态联动:结合 Ability/Component 的生命周期(如onPageShowonDisappear),实现状态的初始化、销毁、缓存;
  4. 异步数据与状态管理:结合鸿蒙的Promiseasync/await,实现网络请求(http)、本地存储(Preferences)的异步数据与状态变量的联动;
  5. 多端适配与状态:结合鸿蒙的设备形态判断媒体查询,实现不同设备的状态差异化展示。

总结

本次鸿蒙原生应用开发进阶,核心围绕ArkTS 状态变量管理展开,从基础到高级,从单一组件到全局跨页面,覆盖了开发中所有常见的状态管理场景,核心关键点回顾:

  1. 鸿蒙状态管理的核心是响应式编程,装饰器标记的状态变量变化会自动触发 UI 刷新,无需手动操作;
  2. 基础装饰器@State/@Prop/@Link实现父子组件状态交互@Link双向绑定需加 $ 传值,@Prop单向只读;
  3. @Provide/@Consume实现跨层级状态共享,无需逐层传值,通过同名绑定实现双向同步;
  4. @StorageLink结合AppStorage实现全局跨页面状态共享,是跨页面交互的核心;
  5. 复杂类型(Object/Array)的核心坑是直接修改子属性不触发刷新,解决方案:重新赋值(新手首选)、@Observed + @ObjectLink(自定义类);
  6. 开发中需遵循装饰器粒度最小化、状态变量私有化、组件化拆分的规范,避免常见避坑点。
Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐