鸿蒙应用开发实战【08】— AppStorage 全局状态与跨页面通信
·
鸿蒙应用开发实战【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 完整流程图
图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 是备用 |
| 键名管理 | 用常量类统一管理,避免拼写错误 |
参考资料
- 鸿蒙应用开发实战【07】— ArkUI 状态管理全解析
- 鸿蒙应用开发实战【13】— AppListItem 组件实现
- AppStorage 文档
- @StorageProp 装饰器
- @StorageLink 装饰器
- PersistentStorage 文档
- router API
- LocalStorage 文档
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
更多推荐




所有评论(0)