为什么需要更高级的状态管理
在第2章中我们学习了 @State、@Prop、@Link 这三个最基础的状态装饰器,它们能解决组件内部状态和父子组件间通信的问题。但真实应用往往更复杂:
- 跨层级状态:主题色、登录用户信息需要在页面任意层级的组件中使用,@Prop 逐层传递(prop drilling)会导致代码臃肿
- 全局状态:购物车数量、未读消息数这类全局数据,需要跨页面共享
- 持久化状态:用户偏好设置、登录 token,应用重启后需要恢复
- 嵌套对象监听:当状态是复杂对象时,如何精确感知深层属性变化
@Provide 与 @Consume — 跨层级状态
@Provide 和 @Consume 解决了"prop drilling"问题。祖先组件用 @Provide 提供数据,任意后代组件用 @Consume 消费,不需要逐层传递:
// 根页面:提供主题色状态
@Entry
@Component
struct RootPage {
// @Provide 注册全局状态,字符串是 key
@Provide('theme') currentTheme: string = 'dark'
build() {
Column() {
HeaderBar() // 不需要传参
ContentArea() // 不需要传参
}
}
}
// 深层嵌套的子组件:直接消费
@Component
struct ThemeToggle {
// @Consume 通过相同的 key 获取祖先提供的状态
@Consume('theme') currentTheme: string
build() {
Button(this.currentTheme === 'dark' ? '切换亮色' : '切换暗色')
.onClick(() => {
// 修改 @Consume 变量会同步回 @Provide 组件
this.currentTheme = this.currentTheme === 'dark' ? 'light' : 'dark'
})
}
}
// 另一个深层组件也可以使用同一个状态
@Component
struct ArticleCard {
@Consume('theme') currentTheme: string
build() {
Column() {
// 根据主题动态切换背景色
}
.backgroundColor(this.currentTheme === 'dark' ? '#1c2128' : '#ffffff')
}
}
使用 @Consume 前必须确保 @Provide 存在
如果组件树中不存在对应 key 的 @Provide,@Consume 会在运行时抛出异常。务必保证 @Provide 组件是 @Consume 组件的祖先(在组件树中层级更高)。
AppStorage — 应用级全局状态
AppStorage 是 HarmonyOS 提供的内存级全局状态仓库,整个应用进程共享同一个 AppStorage 实例。它特别适合存储登录状态、全局配置、跨页面共享数据:
AppStorage.setOrCreate(key, value)
设置或创建一个键值对。如果 key 已存在,更新其值;如果不存在,创建新条目。通常在应用启动时(EntryAbility.onCreate)调用,初始化全局状态。
AppStorage.get(key)
获取 key 对应的当前值。注意这是普通读取,不会建立响应式绑定,读到的是当前快照值。
@StorageProp(key)
组件装饰器,将 AppStorage 中的 key 单向绑定到组件属性。AppStorage 中的值更新会自动刷新组件,但组件内修改不会同步回 AppStorage。
@StorageLink(key)
组件装饰器,将 AppStorage 中的 key 双向绑定到组件属性。组件内修改会立即同步到 AppStorage,并触发所有订阅了该 key 的组件重新渲染。
// EntryAbility.ts — 应用启动时初始化全局状态
onCreate() {
// 初始化登录状态
AppStorage.setOrCreate('isLoggedIn', false)
AppStorage.setOrCreate('username', '')
AppStorage.setOrCreate('cartCount', 0)
}
// LoginPage.ets — 登录页面,修改全局状态
@Entry
@Component
struct LoginPage {
// @StorageLink 双向绑定,修改会同步到 AppStorage
@StorageLink('isLoggedIn') isLoggedIn: boolean = false
@StorageLink('username') username: string = ''
@State inputUsername: string = ''
@State inputPassword: string = ''
build() {
Column({ space: 20 }) {
TextInput({ placeholder: '用户名' })
.onChange((v: string) => { this.inputUsername = v })
TextInput({ placeholder: '密码' })
.type(InputType.Password)
.onChange((v: string) => { this.inputPassword = v })
Button('登录')
.onClick(async () => {
const success = await loginApi(this.inputUsername, this.inputPassword)
if (success) {
// 修改 @StorageLink 变量,自动同步到 AppStorage
// 所有使用 @StorageLink('isLoggedIn') 的组件都会自动更新
this.isLoggedIn = true
this.username = this.inputUsername
}
})
}
.padding(24)
}
}
// NavBar.ets — 导航栏读取登录状态
@Component
struct NavBar {
// @StorageProp 单向绑定,只读
@StorageProp('isLoggedIn') isLoggedIn: boolean = false
@StorageProp('username') username: string = ''
build() {
Row() {
Text(this.isLoggedIn ? this.username : '未登录')
Blank()
if (this.isLoggedIn) {
Button('退出')
.onClick(() => {
AppStorage.set('isLoggedIn', false)
AppStorage.set('username', '')
})
}
}
}
}
PersistentStorage — 持久化状态
PersistentStorage 在 AppStorage 的基础上增加了磁盘持久化能力。被 PersistentStorage 关联的 key,其值会自动保存到设备存储,应用重启后恢复,无需手动读写文件:
// EntryAbility.ts — 在 onCreate 中注册需要持久化的 key
onCreate() {
// PersistentStorage 必须在使用 @StorageProp/@StorageLink 之前调用
PersistentStorage.persistProp('userTheme', 'dark') // 主题设置
PersistentStorage.persistProp('fontSize', 16) // 字体大小
PersistentStorage.persistProp('notifyEnabled', true) // 通知开关
// 批量持久化
PersistentStorage.persistProps([
{ key: 'language', defaultValue: 'zh-CN' },
{ key: 'autoPlay', defaultValue: false }
])
}
// SettingsPage.ets — 用户偏好设置页
@Entry
@Component
struct SettingsPage {
// 通过 @StorageLink 绑定,修改会自动持久化到磁盘
@StorageLink('userTheme') theme: string = 'dark'
@StorageLink('fontSize') fontSize: number = 16
@StorageLink('notifyEnabled') notifyEnabled: boolean = true
build() {
Column({ space: 16 }) {
Text('外观设置').fontSize(18).fontWeight(FontWeight.Bold)
Row() {
Text('深色模式')
Blank()
// Toggle 开关,双向绑定
Toggle({ type: ToggleType.Switch, isOn: this.theme === 'dark' })
.onChange((isOn: boolean) => {
this.theme = isOn ? 'dark' : 'light'
// 这一行修改会自动写入磁盘,下次启动时恢复
})
}
Row() {
Text(`字体大小:${this.fontSize}`)
Blank()
Slider({
value: this.fontSize,
min: 12,
max: 24,
style: SliderStyle.InSet
})
.onChange((value: number) => {
this.fontSize = Math.round(value)
})
}
}
.padding(24)
}
}
PersistentStorage 的存储位置
PersistentStorage 将数据存储在应用的沙箱目录中,格式为 JSON 文件。每次修改 @StorageLink 绑定的值时,框架异步将变更写入磁盘。应用卸载时,这些数据也会随之清除。
@Observed 与 @ObjectLink — 嵌套对象监听
一个常见的坑:当 @State 变量是一个对象时,直接修改对象的属性不会触发 UI 更新,因为 @State 只监听变量引用的变化,不会深入监听对象内部:
// 错误示例:修改对象属性不触发更新
interface Product {
name: string
price: number
inStock: boolean
}
@State product: Product = { name: '鸿蒙手机', price: 3999, inStock: true }
// 下面这行不会触发 UI 刷新!
this.product.price = 4999 // product 引用没有变,@State 检测不到
// 正确做法:替换整个对象引用(触发 @State 检测)
this.product = { ...this.product, price: 4999 } // 展开运算符创建新对象
对于复杂的嵌套结构,每次修改属性都要创建新对象很麻烦。这时应使用 @Observed + @ObjectLink:
// @Observed 标记类,使其属性的变化可被感知
@Observed
class ShoppingItem {
id: number
name: string
price: number
quantity: number
constructor(id: number, name: string, price: number) {
this.id = id
this.name = name
this.price = price
this.quantity = 1
}
get total(): number {
return this.price * this.quantity
}
}
// 列表项组件:用 @ObjectLink 接收 @Observed 对象
@Component
struct CartItemView {
// @ObjectLink 与 @Observed 对象双向绑定
// 修改 item 的属性会精确触发依赖它的 UI 更新
@ObjectLink item: ShoppingItem
build() {
Row({ space: 12 }) {
Text(this.item.name).fontSize(16).layoutWeight(1)
Row({ space: 8 }) {
Button('-').onClick(() => {
if (this.item.quantity > 1) this.item.quantity--
// 直接修改 @Observed 类的属性,UI 自动更新
})
Text(this.item.quantity.toString())
Button('+').onClick(() => { this.item.quantity++ })
}
Text(`¥${this.item.total}`).fontColor('#CF0A2C')
}
.padding(16)
.backgroundColor('#1c2128')
.borderRadius(10)
}
}
// 购物车页面
@Entry
@Component
struct CartPage {
@State cartItems: ShoppingItem[] = [
new ShoppingItem(1, 'Mate 60 Pro', 6999),
new ShoppingItem(2, 'MatePad Pro', 3999),
]
get totalAmount(): number {
return this.cartItems.reduce((sum, item) => sum + item.total, 0)
}
build() {
Column({ space: 12 }) {
ForEach(this.cartItems, (item: ShoppingItem) => {
// 传 @Observed 对象给 @ObjectLink 子组件
CartItemView({ item: item })
})
Row() {
Text('合计').fontSize(18).fontWeight(FontWeight.Bold)
Blank()
Text(`¥${this.totalAmount}`)
.fontSize(20)
.fontWeight(FontWeight.Bold)
.fontColor('#CF0A2C')
}
.padding(16)
}
.padding(16)
}
}
状态管理选型指南
面对这么多状态装饰器,如何在实际项目中选择?遵循以下决策树:
状态管理选型决策树
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
这个状态只在当前组件内部使用?
├── 是 → @State
└── 否 → 需要跨组件共享
├── 只是父子之间?
│ ├── 父→子单向 → @Prop
│ └── 父子双向 → @Link
├── 跨多个层级?
│ └── @Provide + @Consume
├── 跨页面/全应用范围?
│ ├── 不需要持久化 → AppStorage + @StorageLink
│ └── 需要持久化 → PersistentStorage + @StorageLink
└── 状态是复杂对象,需精确监听属性?
└── @Observed 类 + @ObjectLink
Environment — 设备环境信息
Environment 是 ArkUI 提供的系统环境信息访问接口,可以获取当前设备的语言、深浅色模式、字体缩放比例等,并将其注入 AppStorage:
// 在 EntryAbility.onCreate 中注入环境变量
onCreate() {
// 将设备颜色模式注入 AppStorage(自动响应系统主题切换)
Environment.envProp('colorMode', ColorMode.LIGHT)
Environment.envProp('languageCode', 'zh')
Environment.envProp('fontSizeScale', 1)
}
// 在组件中使用
@Entry
@Component
struct ThemedApp {
// 系统主题色模式,随用户切换系统深浅色自动更新
@StorageProp('colorMode') colorMode: ColorMode = ColorMode.LIGHT
build() {
Column() {
Text('自适应主题')
.fontColor(this.colorMode === ColorMode.DARK ? '#ffffff' : '#000000')
}
.backgroundColor(this.colorMode === ColorMode.DARK ? '#0d1117' : '#f5f5f5')
}
}
最佳实践:状态初始化顺序
在 EntryAbility.onCreate() 中,正确的初始化顺序是:
1. 先调用 PersistentStorage.persistProp() 注册需要持久化的 key
2. 再调用 Environment.envProp() 注入环境变量
3. 最后调用 AppStorage.setOrCreate() 初始化其他全局状态
这个顺序保证了持久化状态在应用逻辑使用前已从磁盘恢复。