前言

做鸿蒙开发,绕不开一个问题:用户登录了,首页要显示昵称,"我的"页面要显示头像,设置页要展示会员等级。多个页面共享同一份数据,怎么搞?

@State@Prop 搞不定跨组件通信,这时候就该 AppStorage 出场了。但如果用户杀了应用再打开,你还想保留这些数据呢?那得加上 PersistentStorage

今天把这两个东西掰清楚了讲,再给个实际项目中的搭配方案。

Notion-style clean layout diagram explaining Harmo

AppStorage:内存级的全局仓库

AppStorage 是个单例的键值对存储,整个应用生命周期内有效。你往里塞个值,任何组件都能读出来。

核心 API 很简单:

// 写入
AppStorage.setOrCreate<string>('userName', '张三')
AppStorage.setOrCreate<number>('userId', 10086)

// 读取
let name = AppStorage.get<string>('userName')  // '张三'

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

// 检查是否存在
AppStorage.has('userName')  // false

在组件里用起来更顺手,用 @StorageProp@StorageLink

@Component
struct UserProfile {
  // 只读——AppStorage 变了,UI 会刷新,但改这个变量不会写回
  @StorageProp('userName') userName: string = ''

  // 双向——改了这个变量,AppStorage 里的值也变
  @StorageLink('themeColor') themeColor: string = '#3498db'

  build() {
    Column() {
      Text(`欢迎回来,${this.userName}`)
        .fontSize(18)

      Button('切换主题色')
        .onClick(() => {
          this.themeColor = this.themeColor === '#3498db' ? '#e74c3c' : '#3498db'
        })
        .backgroundColor(this.themeColor)
    }
  }
}

Notion-style two-column comparison table for ArkTS

@StorageProp 是单向的,组件读 AppStorage 的值,但改本地变量不会影响全局。@StorageLink 是双向的,改一头另一头跟着动。

AppStorage 的局限:它是纯内存的。应用被系统杀掉、用户手动退出,数据就没了。下次打开又是白纸一张。

PersistentStorage:能活过重启的存储

PersistentStorage 解决的就是持久化问题。它把数据写到磁盘上,应用重启后自动恢复。

用法也不复杂:

// 在应用启动时调用,通常在 UIAbility 的 onCreate 里
PersistentStorage.persistProp<string>('userName', '')
PersistentStorage.persistProp<string>('themeColor', '#3498db')
PersistentStorage.persistProp<string>('language', 'zh-CN')

persistProp 的意思是:如果磁盘上有这个 key 的值,就恢复到 AppStorage 里;如果没有,就用给的默认值。

之后在组件里照常用 @StorageProp@StorageLink 读写,完全不用关心底层是内存还是磁盘。PersistentStorage 自动在后台同步。

两者配合的最佳实践

实际项目中,不是所有数据都需要持久化。我一般这么分:

数据类型 存储方式 例子
需要持久化的设置 PersistentStorage + @StorageLink 主题色、语言、字体大小
临时全局状态 AppStorage + @StorageLink 登录态、当前页面索引
敏感信息 不存这里 Token 走安全存储

关键原则:PersistentStorage 负责"恢复现场",AppStorage 负责"运行时共享"

Notion-style architectural flowchart for HarmonyOS

实战:登录态 + 主题色 + 语言的全局管理

先看应用入口,在 UIAbility 里初始化持久化存储:

// entry/src/main/ets/entryability/EntryAbility.ets
import UIAbility from '@ohos.app.ability.UIAbility'

export default class EntryAbility extends UIAbility {
  onCreate(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    // 持久化这些需要跨重启保留的设置
    PersistentStorage.persistProp<string>('themeColor', '#3498db')
    PersistentStorage.persistProp<string>('language', 'zh-CN')
    PersistentStorage.persistProp<number>('fontSize', 16)
    PersistentStorage.persistProp<boolean>('isDarkMode', false)

    // 登录态不需要持久化(安全考虑)
    AppStorage.setOrCreate<boolean>('isLoggedIn', false)
    AppStorage.setOrCreate<string>('userName', '')
  }
}

注意:Token、密码这类敏感信息不要往 PersistentStorage 里塞。它底层用的是 Preferences,数据是明文存储的。敏感信息应该走 @ohos.security.keystore 或业务侧加密后存储。

然后搞一个设置页面:

@Component
struct SettingsPage {
  @StorageLink('themeColor') themeColor: string = '#3498db'
  @StorageLink('language') language: string = 'zh-CN'
  @StorageLink('fontSize') fontSize: number = 16
  @StorageLink('isDarkMode') isDarkMode: boolean = false
  @StorageLink('isLoggedIn') isLoggedIn: boolean = false
  @StorageLink('userName') userName: string = ''

  build() {
    Column({ space: 20 }) {
      Text('设置')
        .fontSize(22)
        .fontWeight(FontWeight.Bold)

      // 主题色选择
      Row() {
        Text('主题色')
          .fontSize(this.fontSize)
        Blank()
        Row({ space: 12 }) {
          Circle({ width: 30, height: 30 })
            .fill('#3498db')
            .onClick(() => { this.themeColor = '#3498db' })
          Circle({ width: 30, height: 30 })
            .fill('#e74c3c')
            .onClick(() => { this.themeColor = '#e74c3c' })
          Circle({ width: 30, height: 30 })
            .fill('#2ecc71')
            .onClick(() => { this.themeColor = '#2ecc71' })
        }
      }
      .width('100%')
      .padding(12)

      // 语言选择
      Row() {
        Text('语言')
          .fontSize(this.fontSize)
        Blank()
        Text(this.language === 'zh-CN' ? '中文' : 'English')
          .fontSize(this.fontSize)
          .onClick(() => {
            this.language = this.language === 'zh-CN' ? 'en-US' : 'zh-CN'
          })
      }
      .width('100%')
      .padding(12)

      // 字体大小
      Row() {
        Text('字体大小')
          .fontSize(this.fontSize)
        Blank()
        Slider({
          value: this.fontSize,
          min: 12,
          max: 24,
          step: 1
        })
          .width(150)
          .onChange((value: number) => {
            this.fontSize = value
          })
        Text(`${this.fontSize}`)
          .width(30)
      }
      .width('100%')
      .padding(12)

      // 暗色模式
      Row() {
        Text('暗色模式')
          .fontSize(this.fontSize)
        Blank()
        Toggle({ type: ToggleType.Switch, isOn: this.isDarkMode })
          .onChange((isOn: boolean) => {
            this.isDarkMode = isOn
          })
      }
      .width('100%')
      .padding(12)

      // 模拟登录/登出
      Button(this.isLoggedIn ? '退出登录' : '登录')
        .backgroundColor(this.isLoggedIn ? '#999' : this.themeColor)
        .onClick(() => {
          if (this.isLoggedIn) {
            this.isLoggedIn = false
            this.userName = ''
          } else {
            this.isLoggedIn = true
            this.userName = '张三'
          }
        })
    }
    .padding(20)
  }
}

首页读取这些设置:

@Component
struct HomePage {
  @StorageProp('userName') userName: string = ''
  @StorageProp('isLoggedIn') isLoggedIn: boolean = false
  @StorageProp('themeColor') themeColor: string = '#3498db'
  @StorageProp('fontSize') fontSize: number = 16

  build() {
    Column() {
      Text(this.isLoggedIn ? `Hi, ${this.userName}` : '请先登录')
        .fontSize(this.fontSize + 4)
        .fontColor(this.themeColor)

      Text('这里是首页内容区域')
        .fontSize(this.fontSize)
        .margin({ top: 20 })
    }
    .width('100%')
    .padding(20)
  }
}

用户在设置页改了主题色,首页的标题颜色立刻跟着变。杀应用重启,主题色还是上次选的那个,但登录状态没了——因为登录态走的是 AppStorage,不持久化。

几个实用建议

不要在 PersistentStorage 里存大对象。它底层用的 Preferences 有大小限制,存个字符串、数字、布尔值没问题,塞个几百 KB 的 JSON 就不合适了。大数据走关系型数据库或文件。

初始化的时机要早PersistentStorage.persistProp 要在任何组件读取之前调用,最靠谱的地方就是 UIAbility.onCreate。放在页面组件的 aboutToAppear 里可能会来不及。

命名要规范。key 都是全局的,不同模块之间容易撞名。我建议加前缀,比如 settings.themeColoruser.name

最后聊两句

AppStoragePersistentStorage 这对搭档,用起来其实挺简单的。核心就是分清楚哪些数据要持久化、哪些不用。

真正让人纠结的是"什么时候该用全局状态"。我的原则是:能不用就不用。如果数据只在一个组件树里流转,老老实实用 @State + @Prop + @Link 往下传。全局状态是个诱惑,用多了会让代码变得难追踪。等数据真正需要跨页面、跨模块共享了,再把 AppStorage 请出来。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐