鸿蒙原生应用开发进阶:ArkTS 状态变量核心管理实战
在上一篇基础开发教程的基础上,本次聚焦鸿蒙原生开发的核心核心 —— 状态变量管理(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 核心原则(避坑关键)
- 状态变量只能在 UI 主线程修改:鸿蒙框架仅监听 UI 线程的状态变化,子线程修改需通过
UIContext切回主线程; - 基础类型 vs 复杂类型:
number/string/boolean等基础类型可直接修改值触发刷新;Object/Array/自定义类等复杂类型,直接修改子属性不会触发刷新,需通过重新赋值或深度监听装饰器实现; - 装饰器不可滥用:按作用域选择最小粒度的装饰器(如仅组件内部使用用
@State,而非全局的@StorageLink),减少框架监听开销; - 状态变量尽量私有化:标记为
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 关键知识点解析
- @Link 传值必须加
$符号:父组件向子组件传递@Link变量时,需用$变量名(如$totalCount),表示传递变量的引用,而非单纯的值,这是双向绑定的核心; - @Prop 传值直接传变量:
@Prop是单向值传递,直接传this.totalCount即可,子组件修改该变量不会同步到父组件(若子组件尝试修改,编译器会报错); - 子组件无需 @Entry:只有页面入口组件需要
@Entry标记,自定义子组件仅需@Component,可被其他组件无限引用; - 状态变化的联动性:点击 + 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 关键知识点解析
- 同名绑定原则:
@Provide和@Consume的变量名必须完全一致(如都为themeColor),框架通过变量名实现自动绑定,无需手动关联; - 跨层级无限制:无论嵌套多少层(父→子→孙→重孙),只要后代组件用
@Consume同名标记,就能直接获取祖先组件的@Provide状态; - 双向同步:后代组件修改
@Consume变量,会同步到祖先组件的@Provide变量,反之亦然,实现跨层级双向联动; - 作用域限制:
@Consume只能绑定最近的祖先组件的@Provide状态(若多个祖先组件有同名@Provide,优先绑定最近的一个)。
3.5 运行效果
点击「切换主题色」按钮,祖先组件的按钮背景色和孙组件的色块背景色会同步随机变化,中间的子组件无需做任何状态处理,实现了跨层级的状态共享。
四、全局状态管理(@StorageLink)
在鸿蒙应用开发中,跨页面状态共享(如「首页→个人中心→设置页」共享用户信息、主题设置、登录状态)是常见需求,@State/@Provide等装饰器的作用域仅限于组件树内部,无法实现跨页面共享。
鸿蒙提供了全局状态存储(AppStorage),结合@StorageLink装饰器,可实现整个应用的全局状态共享,所有页面 / 组件都能绑定并修改全局状态,数据实时同步。
4.1 核心概念:AppStorage
AppStorage是鸿蒙框架提供的全局单例存储容器,生命周期与应用一致(应用启动时创建,应用退出时销毁),用于存储全局共享的状态数据。
@StorageLink:将组件的状态变量与AppStorage中的指定键值双向绑定,组件修改变量会同步到AppStorage,AppStorage中的值变化会同步到所有绑定的组件;- 补充:
@StorageProp:单向绑定,组件只能读取AppStorage的值,不能修改(适合全局只读状态,如应用版本、全局配置)。
4.2 实战场景设计
实现跨页面全局计数器:
- 全局状态:在
AppStorage中定义全局计数器globalCount,初始值为 0; - 页面 1(GlobalPage1.ets):绑定全局计数器,实现 + 1 操作,展示当前值;
- 页面 2(GlobalPage2.ets):绑定全局计数器,实现 - 1 操作,展示当前值;
- 路由跳转:两个页面互相跳转,跳转后计数器的全局状态保持一致,不会重置。
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 关键知识点解析
- 键值绑定原则:
@StorageLink('键名')通过键名绑定AppStorage中的全局数据,多个页面绑定同一个键名,即可实现状态共享; - 自动初始化:若
AppStorage中无指定键名,@StorageLink会将组件的初始值(如0)自动添加到AppStorage中;若已有该键名,组件的初始值会被AppStorage中的值覆盖; - 全局双向同步:任意页面修改
@StorageLink变量,会同步到AppStorage,所有绑定该键名的页面 / 组件会自动刷新 UI,实现全局状态联动; - 数据持久化:
AppStorage是内存级存储,应用退出后数据会丢失;若需要持久化全局状态(如登录状态、用户信息),可结合鸿蒙的Preferences(轻量存储)实现「内存 + 本地」双重存储; - @StorageProp 用法:与
@StorageLink语法一致(@StorageProp('globalCount')),但仅能读取全局状态,不能修改,适合展示类全局数据。
4.7 运行效果
- 在页 1 点击「全局 + 1」,计数器数值增加,跳转到页 2 后,计数器数值与页 1 保持一致;
- 在页 2 点击「全局 - 1」,计数器数值减少,返回页 1 后,数值同步更新;
- 两个页面的计数器状态全局共享,不会因页面跳转而重置。
五、复杂类型状态管理(Object/Array)
前面的示例都是基础类型(number/string)的状态管理,实际开发中,我们经常需要处理复杂类型(Object:用户信息、商品信息;Array:列表数据、菜单数据)的状态。
由于 ArkTS 的状态监听仅监听变量的引用 / 值变化,直接修改复杂类型的子属性 / 内部值,不会触发 UI 刷新,这是开发中最常见的坑,本节重点讲解复杂类型的正确管理方式。
5.1 核心解决方案
针对复杂类型的状态管理,鸿蒙提供3 种核心解决方案,按易用性排序,新手优先掌握前两种:
方案 1:重新赋值(推荐,新手首选)
对 Object/Array 进行浅拷贝重新赋值,修改引用地址,触发框架的状态监听,实现 UI 刷新。
- Object:使用扩展运算符(
...)浅拷贝,修改后重新赋值; - Array:使用扩展运算符(
...)、filter、map等方法,返回新数组后重新赋值(避免使用push/pop/shift/unshift等直接修改原数组的方法)。
方案 2:@Observed + @ObjectLink(自定义类深度监听)
若使用自定义类(如User、Goods),可通过@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 关键知识点解析
- 重新赋值的核心:通过扩展运算符、
filter、map等方法,生成新的 Object/Array,修改状态变量的引用地址,让框架监听到状态变化; - ForEach 唯一标识:遍历数组渲染列表时,
ForEach的第三个参数必须传递唯一标识(如 item 本身、index+item),否则框架无法准确识别列表项的变化,导致 UI 刷新异常; - @Observed 仅标记类:
@Observed只能修饰自定义类,不能修饰 Object/Array,且必须与@ObjectLink配合使用; - @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 刷新:
- 点击「切换主题色」,按钮背景色和数量修改按钮的背景色跨层级同步变化;
- 点击数量的「+/-」,购买数量和总价实时联动变化;
- 点击「添加推荐商品」,推荐商品列表实时新增项;
- 所有复杂类型(Object/Array)的修改均能正确触发 UI 刷新,无任何卡顿或不刷新问题。
七、状态管理开发规范与避坑指南
7.1 核心开发规范
- 装饰器粒度最小化:按作用域选择最合适的装饰器,避免用全局装饰器(@StorageLink)管理组件内部状态;
- 状态变量私有化:所有状态变量均标记为
private,通过组件方法统一管理状态变化(如updateCount()),避免外部直接修改; - 复杂类型统一管理:对 Object/Array 的修改封装为专用方法,避免在多处直接重新赋值,提高代码可维护性;
- 计算属性替代重复逻辑:对于基于状态变量的计算逻辑(如总价格、列表长度),使用计算属性(
get totalPrice()),无需标记装饰器,依赖的状态变化会自动触发计算; - 组件化拆分:将复杂页面拆分为多个独立子组件,每个子组件仅管理自身的状态,通过装饰器实现组件间的状态交互,符合 “单一职责原则”。
7.2 常见避坑点
- 直接修改复杂类型子属性:忘记重新赋值,导致 UI 不刷新,解决方案:扩展运算符浅拷贝重新赋值;
- **@Link 传值忘记加:编译器报错,解决方案:父组件向子组件传递变量时,必须加符号;
- @Provide/@Consume 变量名不一致:跨层级状态绑定失败,解决方案:保证变量名完全一致;
- ForEach 缺少唯一标识:列表刷新异常、重复渲染,解决方案:传递第三个参数作为唯一标识;
- 子线程修改状态变量:UI 不刷新甚至应用闪退,解决方案:通过
UIContext切回 UI 主线程修改; - 滥用 @StorageLink:所有状态都用全局管理,导致应用性能下降,解决方案:仅跨页面共享的状态使用 @StorageLink;
- 自定义类忘记加 @Observed:直接修改子属性不触发刷新,解决方案:用 @Observed 标记自定义类,配合 @ObjectLink 使用。
八、后续进阶学习方向
掌握本次的核心状态管理知识后,你已具备开发中大型鸿蒙原生应用的基础,后续可围绕以下方向进阶:
- 高级状态管理:学习
LocalStorage(页面级全局存储)、AppStorage与Preferences结合实现持久化全局状态; - 状态管理框架:学习鸿蒙生态的第三方状态管理框架(如
OhRedux、ArkMobx),适配复杂应用的状态管理; - 生命周期与状态联动:结合 Ability/Component 的生命周期(如
onPageShow、onDisappear),实现状态的初始化、销毁、缓存; - 异步数据与状态管理:结合鸿蒙的
Promise、async/await,实现网络请求(http)、本地存储(Preferences)的异步数据与状态变量的联动; - 多端适配与状态:结合鸿蒙的设备形态判断、媒体查询,实现不同设备的状态差异化展示。
总结
本次鸿蒙原生应用开发进阶,核心围绕ArkTS 状态变量管理展开,从基础到高级,从单一组件到全局跨页面,覆盖了开发中所有常见的状态管理场景,核心关键点回顾:
- 鸿蒙状态管理的核心是响应式编程,装饰器标记的状态变量变化会自动触发 UI 刷新,无需手动操作;
- 基础装饰器
@State/@Prop/@Link实现父子组件状态交互,@Link双向绑定需加 $ 传值,@Prop单向只读; @Provide/@Consume实现跨层级状态共享,无需逐层传值,通过同名绑定实现双向同步;@StorageLink结合AppStorage实现全局跨页面状态共享,是跨页面交互的核心;- 复杂类型(Object/Array)的核心坑是直接修改子属性不触发刷新,解决方案:重新赋值(新手首选)、
@Observed + @ObjectLink(自定义类); - 开发中需遵循装饰器粒度最小化、状态变量私有化、组件化拆分的规范,避免常见避坑点。
更多推荐


所有评论(0)