鸿蒙应用开发实战【08】— AppStorage 全局状态与跨页面通信

前言

欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
在这里插入图片描述

在号码助手中,当用户点击应用列表中的某一项时,需要把被点击项的 bindingId 传递给 AppDetailPage。HarmonyOS 的路由参数传递 router.pushUrl({ params: {...} }) 在子组件中存在不可靠问题,因此我们使用 AppStorage 作为跨页面通信的主通道。本篇深入讲解 AppStorage 的原理、使用模式,以及与路由参数的对比。

本篇涵盖:AppStorage 原理、setOrCreate / get / set、@StorageProp / @StorageLink 装饰器、router.getParams 的局限性、跨页面传参的完整实践。


一、AppStorage 概述

1.1 什么是 AppStorage

AppStorage 是 ArkUI 框架提供的应用级全局键值存储,在整个应用生命周期内存在,所有 UIAbility 和页面都可访问。

特性 AppStorage LocalStorage PersistentStorage
作用域 应用全局 页面/组件 应用全局+持久化
生命周期 进程生命周期 组件生命周期 持久化到磁盘
响应式
持久化 ❌(重启清空)
适用场景 跨页面状态共享 组件树内部 用户偏好设置

1.2 AppStorage 的核心 API

// 写入(如不存在则创建,如已存在则更新)
AppStorage.setOrCreate<number>('current_binding_id', 42)
AppStorage.setOrCreate<string>('user_theme', 'light')

// 读取
const id = AppStorage.get<number>('current_binding_id')

// 更新已有值
AppStorage.set<number>('current_binding_id', 99)

// 删除
AppStorage.delete('current_binding_id')

// 检查是否存在
const exists = AppStorage.has('current_binding_id')

二、号码助手中的跨页面传参方案

2.1 问题背景

// AppListItem.ets — 子组件中导航到详情页
// 方案1:router params(问题方案)
this.getUIContext().getRouter().pushUrl({
  url: 'pages/AppDetailPage',
  params: { bindingId: this.bindingId }   // ⚠️ 在子组件中可能不可靠
})

// AppDetailPage.ets — 接收参数
aboutToAppear(): void {
  const params = router.getParams() as Record<string, number>
  // ⚠️ params 有时为 null 或值为 0
  this.bindingId = params?.['bindingId'] ?? 0
}

根本原因router.getParams() 在子组件(非 @Entry 页面)发起的导航中,参数传递存在时机问题。

2.2 解决方案:AppStorage 双保险

// AppListItem.ets — 先写入 AppStorage,再导航
.onClick(() => {
  if (this.bindingId <= 0) {
    promptAction.showToast({ message: '数据异常,请刷新后重试' })
    return
  }
  try {
    // 主通道:AppStorage(可靠)
    AppStorage.setOrCreate<number>('current_binding_id', this.bindingId)
    // 次通道:router params(备用)
    router.pushUrl({
      url: 'pages/AppDetailPage',
      params: { bindingId: this.bindingId }
    })
  } catch (err) {
    promptAction.showToast({ message: '打开失败,请重试' })
  }
})
// AppDetailPage.ets — 读取 AppStorage
@Entry
@Component
struct AppDetailPage {
  // 主通道:@StorageProp 直接绑定 AppStorage
  @StorageProp('current_binding_id') bindingId: number = 0

  aboutToAppear(): void {
    // 次通道:router params 作为备用
    const params = router.getParams() as Record<string, number>
    if (params?.['bindingId'] && this.bindingId <= 0) {
      this.bindingId = params['bindingId']
    }
    this.loadData()
  }
}

三、@StorageProp — 单向绑定 AppStorage

3.1 基本用法

@Entry
@Component
struct AppDetailPage {
  // @StorageProp:AppStorage → 组件(单向,只读)
  // 'current_binding_id' 是 AppStorage 中的键名
  @StorageProp('current_binding_id') bindingId: number = 0
  //                                              ↑ 默认值(AppStorage 中无此键时使用)
}

3.2 @StorageProp vs AppStorage.get 的区别

特性 @StorageProp AppStorage.get
声明位置 类成员变量 方法内
响应式 ✅ 自动刷新 UI ❌ 需手动读取
更新时机 AppStorage 变化时自动同步 每次调用才读
使用场景 需要随 AppStorage 变化刷新 UI 一次性读取
// 场景:需要响应 AppStorage 变化时用 @StorageProp
@StorageProp('current_theme') theme: string = 'light'
// theme 变化时,使用它的 UI 自动刷新

// 场景:一次性读取用 AppStorage.get
aboutToAppear(): void {
  const lastId = AppStorage.get<number>('last_viewed_id') ?? 0
  // 只需要初始值,不需要响应后续变化
}

四、@StorageLink — 双向绑定 AppStorage

4.1 基本用法

@Entry
@Component
struct SettingsPage {
  // @StorageLink:AppStorage ↔ 组件(双向,可读写)
  @StorageLink('user_theme') theme: string = 'light'

  build() {
    Column() {
      Toggle({ type: ToggleType.Switch, isOn: this.theme === 'dark' })
        .onChange((isOn: boolean) => {
          // 修改 theme → 自动同步到 AppStorage → 所有订阅者刷新
          this.theme = isOn ? 'dark' : 'light'
        })
    }
  }
}

4.2 跨页面实时同步示例

// PageA 设置主题
@StorageLink('user_theme') themeA: string = 'light'
// 修改 themeA → AppStorage 更新 → PageB 的 themeB 自动刷新

// PageB 读取主题(无需任何事件监听)
@StorageLink('user_theme') themeB: string = 'light'

五、完整跨页面通信流程

5.1 AppListItem → AppDetailPage 完整流程图

AppStorage 跨页面通信流程

图1:AppListItem 点击 → AppStorage 写入 → 路由跳转 → AppDetailPage 读取的完整链路

用户点击列表项
      │
      ↓
AppListItem.onClick()
  ├── AppStorage.setOrCreate('current_binding_id', bindingId)  ← 写入全局
  └── router.pushUrl({ url: 'pages/AppDetailPage', params: { bindingId } })
                │
                ↓
      页面路由跳转
                │
                ↓
AppDetailPage.aboutToAppear()
  ├── @StorageProp 自动读取 'current_binding_id'  ← 主通道
  └── router.getParams() 读取 params.bindingId    ← 备用通道
                │
                ↓
        this.loadData()  → AppBindingDao.findById(bindingId)

5.2 完整代码

// ── AppListItem.ets(发送方)──
import { promptAction, router } from '@kit.ArkUI'
import { AppStorage } from '@kit.ArkUI'

@Component
export struct AppListItem {
  @Prop bindingId: number = 0

  build() {
    Row() { /* UI */ }
    .onClick(() => {
      if (this.bindingId <= 0) {
        promptAction.showToast({ message: '数据异常,请刷新后重试' })
        return
      }
      try {
        AppStorage.setOrCreate<number>('current_binding_id', this.bindingId)
        router.pushUrl({
          url: 'pages/AppDetailPage',
          params: { bindingId: this.bindingId }
        }, router.RouterMode.Standard, (err: Error) => {
          if (err) {
            promptAction.showToast({ message: `打开失败:${err.message}` })
          }
        })
      } catch (e) {
        promptAction.showToast({ message: '打开失败,请重试' })
      }
    })
  }
}
// ── AppDetailPage.ets(接收方)──
import { router } from '@kit.ArkUI'
import { AppBindingDao } from '../features/data/AppBindingDao'
import { AppBindingEntity } from '../features/data/Models'

@Entry
@Component
struct AppDetailPage {
  @StorageProp('current_binding_id') bindingId: number = 0
  @State loading: boolean = true
  @State loadError: boolean = false
  @State binding: AppBindingEntity | null = null

  aboutToAppear(): void {
    // 次通道兜底
    const params = router.getParams() as Record<string, number>
    if (params?.['bindingId'] && this.bindingId <= 0) {
      this.bindingId = params['bindingId']
    }
    this.loadData()
  }

  private async loadData(): Promise<void> {
    if (this.bindingId <= 0) {
      this.loadError = true
      this.loading = false
      return
    }
    try {
      this.binding = await AppBindingDao.findById(this.bindingId)
      if (!this.binding) {
        promptAction.showToast({ message: '该绑定已被删除' })
        router.back()
      }
    } catch (e) {
      this.loadError = true
    } finally {
      this.loading = false
    }
  }
}

六、AppStorage 使用规范

6.1 键名命名规范

// ✅ 使用常量管理键名,避免字符串拼写错误
export class StorageKeys {
  static readonly CURRENT_BINDING_ID = 'current_binding_id'
  static readonly USER_THEME = 'user_theme'
  static readonly LAST_VIEWED_CARD = 'last_viewed_card'
}

// 使用
AppStorage.setOrCreate<number>(StorageKeys.CURRENT_BINDING_ID, id)

6.2 清理过时数据

// 页面销毁时清理临时数据(可选)
aboutToDisappear(): void {
  // 清理:避免旧 ID 被下次进入页面时误用
  AppStorage.delete('current_binding_id')
}

6.3 不要滥用 AppStorage

// ❌ 滥用:把所有状态都放 AppStorage
AppStorage.setOrCreate('loading', true)         // 应该是组件 @State
AppStorage.setOrCreate('currentFilter', '全部') // 应该是组件 @State

// ✅ 只存跨页面需要共享的状态
AppStorage.setOrCreate('current_binding_id', id)   // ✅ 跨页面传参
AppStorage.setOrCreate('user_settings', settings)  // ✅ 全局偏好

七、AppStorage vs PersistentStorage

// PersistentStorage:持久化到磁盘,应用重启后数据不丢失
// 必须在 AppStorage 之前初始化
PersistentStorage.persistProp('user_theme', 'light')
// 之后通过 AppStorage 操作
AppStorage.get<string>('user_theme')  // 读取(已持久化的值)

// 应用场景对比
// AppStorage:当次运行中的跨页面传参(bindingId 等)
// PersistentStorage:需要持久保存的用户偏好(主题、语言等)

八、本篇小结

知识点 核心要点
AppStorage 应用全局 KV 存储,进程生命周期,非持久化
setOrCreate 不存在时创建,存在时更新
@StorageProp 单向绑定,AppStorage → 组件(只读)
@StorageLink 双向绑定,AppStorage ↔ 组件(可读写)
跨页面传参 AppStorage 是主通道,router params 是备用
键名管理 用常量类统一管理,避免拼写错误

参考资料

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

Logo

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

更多推荐