CHAPTER 03 · 10

状态管理深度

深入理解 @Provide/@Consume、AppStorage 全局状态、PersistentStorage 持久化,掌握数据驱动刷新的完整机制。

为什么需要更高级的状态管理

在第2章中我们学习了 @State、@Prop、@Link 这三个最基础的状态装饰器,它们能解决组件内部状态和父子组件间通信的问题。但真实应用往往更复杂:

@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() 初始化其他全局状态 这个顺序保证了持久化状态在应用逻辑使用前已从磁盘恢复。