关键词:AppStorage、PersistentStorage、LocalStorage、@StorageLink、@StorageProp、全局状态、持久化、状态管理


一、先说痛点:为什么我熬了两个通宵

事情是这样的。上个月我做了一个多页面的应用,首页、个人中心、设置页三个 Tab 页面都需要共享用户登录状态和主题色。我当时天真地想,不就是跨页面传个值嘛,能有多难?结果写完第一版,bug 比功能还多——切个页面状态就丢了,改了主题色另一个页面纹丝不动,@StorageLink key 不存在直接给我崩了个白屏,PersistentStorage 和 AppStorage 同名 key 互相打架……两个通宵就耗在这上面了。

后来静下心把 AppStorage、PersistentStorage、LocalStorage 三套机制从头到尾捋了一遍,才发现好多坑官方文档提了一嘴但没展开,有些坑甚至文档里压根没写。这篇文章就是把我踩过的坑和最终理清的思路全记录下来,希望你能少走弯路。
请添加图片描述


二、AppStorage:全局单例,应用级状态的"大脑"

2.1 它到底是什么

AppStorage 是应用级别的全局状态存储,整个应用只有一个实例,所有页面、所有组件都可以访问。它本质上就是一个全局的 key-value 内存数据库,数据存在内存里,应用退出就没了。

你可以把它理解成一个全局的"状态大脑"——所有组件都从它这里读数据,也可以往它里面写数据,而框架负责在数据变化时通知相关组件刷新 UI。

2.2 初始化:setOrCreate 是第一步,也是最关键的一步

先说最核心的一点:AppStorage 里的 key 必须先初始化,组件才能用。初始化的方法就是 AppStorage.setOrCreate(key, value)

为什么要强调这个?因为后面会讲到的 @StorageLink 装饰器,如果绑定的 key 在 AppStorage 里不存在,应用会直接崩溃。不是报个警告,不是给你个默认值,是直接白屏崩掉。这个坑我踩过不止一次,而且报错信息还不那么直观,排查起来非常痛苦。

正确的做法是在组件使用之前,尽早把所有需要的 key 都 setOrCreate 好。通常在 aboutToAppear 生命周期里做,或者在 EntryAbility 的 onCreate 里做:

aboutToAppear(): void {
  AppStorage.setOrCreate('appCounter', 0);
  AppStorage.setOrCreate('userName', '游客');
  AppStorage.setOrCreate('userRole', '普通用户');
  AppStorage.setOrCreate('userLevel', 1);
  AppStorage.setOrCreate('themeColor', '#1a73e8');
  AppStorage.setOrCreate('readonlyFlag', false);
}

注意 setOrCreate 的语义:如果 key 不存在就创建并赋值,如果 key 已经存在就更新值。所以你放心调用,不会重复创建。

2.3 @StorageLink:双向绑定,改了就同步

@StorageLink 建立的是组件变量和 AppStorage 之间的双向同步关系。用大白话说就是:

  • 组件里改了值 → AppStorage 自动更新
  • AppStorage 里改了值 → 组件自动刷新
  • 其他绑了同一个 key 的组件 → 也会同步更新

数据流是这样的:

组件A修改 ──→ AppStorage更新 ──→ 组件B刷新
                                  组件C刷新
              ←── AppStorage变更也能反向同步到组件A

在我们的 GlobalStateDemo 里,appCounter 就是用 @StorageLink 绑定的全局计数器:

@StorageLink('appCounter') appCounter: number = 0;
@StorageLink('userName') userName: string = '游客';
@StorageLink('userRole') userRole: string = '普通用户';
@StorageLink('userLevel') userLevel: number = 1;
@StorageLink('themeColor') themeColor: string = '#1a73e8';

点击按钮修改 appCounter 时,改动会自动写回 AppStorage:

Button('+')
  .onClick(() => {
    this.appCounter = this.appCounter + 1;
    this.addLog('计数器+1: ' + this.appCounter.toString());
  })

请添加图片描述

这里没有手动调 AppStorage.set(),因为 @StorageLink 是双向的,直接赋值 this.appCounter = xxx 就会触发 AppStorage 的同步更新。如果有另一个页面也用 @StorageLink('appCounter') 绑了同一个 key,那边也会自动刷新——这就是全局状态的威力。

主题色切换也是一个典型场景。五个色块点击后修改 this.themeColor,AppStorage 同步更新,页面上所有用 this.themeColor 渲染的地方(按钮背景色、文字颜色、等级进度条)全部实时跟着变:

Row({ space: 8 }) {
  ForEach(['#1a73e8', '#e65100', '#34a853', '#9c27b0', '#d32f2f'], (color: string) => {
    Row()
      .width(36).height(36).borderRadius(18)
      .backgroundColor(color)
      .border({ width: this.themeColor === color ? 3 : 0, color: '#333333' })
      .onClick(() => {
        this.themeColor = color;
        this.addLog('主题色切换: ' + color);
      })
  }, (color: string, index: number) => index.toString())
}

请添加图片描述

2.4 @StorageProp:单向绑定,只读方向

@StorageProp 建立的是单向同步关系,数据流方向只有一个:

AppStorage ──→ 组件(单向,只读方向)

具体行为:

  • AppStorage 变了 → 组件跟着变
  • 组件里改了值 → 不会写回 AppStorage,只改了本地副本

很多人第一次用 @StorageProp 时会有个误解:觉得"我改了 this.xxx,UI 也跟着变了,那应该是同步了吧?"实际上你改的只是组件本地的值,AppStorage 那边纹丝不动。UI 更新是因为 ArkUI 的状态刷新机制,不是因为你写回了 AppStorage。

在我们的 Demo 里,readonlyFlag 用 @StorageProp 绑定:

@StorageProp('readonlyFlag') readonlyFlag: boolean = false;

然后页面上有两个按钮,一个直接改本地值,一个通过 AppStorage.set() 改全局值:

// 按钮1:直接改本地值 —— AppStorage不会变
Button('切换(仅本地)')
  .onClick(() => {
    this.readonlyFlag = !this.readonlyFlag;
    this.addLog('StorageProp本地切换: ' + this.readonlyFlag.toString() + ' (AppStorage未变)');
  })

// 按钮2:通过AppStorage.set()改全局值 —— 会同步到所有绑定的组件
Button('通过AppStorage改')
  .onClick(() => {
    AppStorage.set('readonlyFlag', !(AppStorage.get<boolean>('readonlyFlag') ?? false));
    this.addLog('AppStorage.set readonlyFlag: ' + String(AppStorage.get<boolean>('readonlyFlag')));
  })

这里的坑非常重要:如果你点了"切换(仅本地)"按钮改了本地值,之后又点了"通过AppStorage改"按钮,AppStorage 的新值会覆盖你本地的修改。这就是 @StorageProp 的设计——AppStorage 是数据源,组件本地值只是它的镜像,AppStorage 一旦更新,本地值就会被覆盖。所以如果你需要在组件内修改值并同步到全局,就别用 @StorageProp,老老实实用 @StorageLink。

2.5 @StorageLink vs @StorageProp:怎么选

简单粗暴的选型原则:

  • 组件需要修改值并同步到全局@StorageLink(比如设置页的主题色选择器、计数器按钮)
  • 组件只展示不修改@StorageProp(比如首页显示用户名、只读标签)

用错了会怎样?用 @StorageProp 去修改值,改了半天发现其他组件不更新,排查半天才发现是单向绑定。用 @StorageLink 去做只读展示倒是不会出 bug,但语义不清晰,给维护的人埋雷。


三、PersistentStorage:AppStorage 的"硬盘版"

3.1 为什么要用 PersistentStorage

AppStorage 的数据存在内存里,应用一退出就没了。但有些数据你希望下次启动还能恢复——比如用户选择的主题色、字体大小、是否开启了夜间模式。这些就需要 PersistentStorage 出场了。

PersistentStorage 可以理解为 AppStorage 的"硬盘镜像"——它和 AppStorage 建立双向同步,你在 AppStorage 里改了值,PersistentStorage 自动帮你写到磁盘;下次应用启动时,它又自动把磁盘上的数据读回来灌进 AppStorage。

3.2 基本用法

PersistentStorage.persistProp('themeColor', '#1a73e8');
PersistentStorage.persistProp('fontSize', 14);

调用 persistProp 后,对应的 key 就会在 AppStorage 和磁盘之间建立同步关系。之后你通过 @StorageLink('themeColor') 修改值,AppStorage 更新的同时,PersistentStorage 也会把新值写到磁盘。

3.3 使用限制和注意事项

PersistentStorage 有几个关键限制你必须知道:

1. 只适合小数据。 官方建议单个键值对不超过 2KB,总量也建议控制在 2KB 以内。为什么?因为 PersistentStorage 的写磁盘操作是同步的,跑在 UI 线程上。你存一大坨数据进去,每次写入都要卡 UI 线程,用户体验直接拉垮。大数据量的持久化请用 Preferences 或关系型数据库,它们是异步的。

2. 必须在 UI 实例初始化之后调用。 PersistentStorage 和 UI 实例关联,如果你在 UI 实例还没初始化好的时候就调 persistProp,持久化会静默失败,没有任何报错。正确的时机是在 loadContent 的回调之后,或者在页面的 aboutToAppear 里调用。

3. 初始化一次就够了。 多次初始化同名 key,最后一次的值会覆盖之前的。别在多个地方重复调用 persistProp

3.4 最要命的坑:和 AppStorage 同名 key 冲突

这个坑我踩得最惨。事情是这样的:

我先在 aboutToAppear 里用 AppStorage.setOrCreate('themeColor', '#1a73e8') 初始化了主题色,然后在另一个地方又用 PersistentStorage.persistProp('themeColor', '#e65100') 做了持久化。结果呢?两者的默认值不一致,应用启动时 PersistentStorage 从磁盘读出来的值和 AppStorage 初始化的值打架,最终表现完全不可预期——有时候是 AppStorage 的值胜出,有时候是 PersistentStorage 的值胜出,取决于初始化顺序。

正确做法:如果某个 key 需要 PersistentStorage 管理,就别再用 AppStorage.setOrCreate 初始化同一个 key。让 PersistentStorage 全权负责——应用首次启动时磁盘上没有数据,它会用 persistProp 的默认值创建;后续启动时它会从磁盘恢复上次的值。AppStorage 这边不需要再做 setOrCreate,因为 PersistentStorage 会自动把数据灌进 AppStorage。

// 错误:两边都初始化同一个key
AppStorage.setOrCreate('themeColor', '#1a73e8');  // AppStorage初始化
PersistentStorage.persistProp('themeColor', '#e65100');  // PersistentStorage也初始化,冲突!

// 正确:只用PersistentStorage管理持久化key
PersistentStorage.persistProp('themeColor', '#1a73e8');  // 只在这初始化
// 不需要再AppStorage.setOrCreate,PersistentStorage会自动同步到AppStorage

四、LocalStorage:页面级共享,不大不小刚刚好

4.1 它解决什么问题

AppStorage 是全局的,任何页面任何组件都能访问。但有时候你不需要全局共享,只需要在同一个 UIAbility 的多个页面之间共享一些数据,这时候用 AppStorage 就有点"杀鸡用牛刀"了——全局状态污染风险太大,所有页面都能读写,稍不注意就互相干扰。

LocalStorage 就是为此而生的:它是页面级的共享存储,作用域限定在一个 UIAbility 内。同一个 UIAbility 下的多个页面可以共享同一个 LocalStorage 实例,但不同 UIAbility 之间互不影响。

4.2 基本用法

首先创建 LocalStorage 实例并注入到页面:

// 在EntryAbility中创建
let storage: LocalStorage = new LocalStorage();
storage.setOrCreate('theme', 'Red');

// 注入到页面
windowStage.loadContent('pages/Index', storage, (err) => {
  // ...
});

然后在页面中通过 @Entry(storage) 接收,用 @LocalStorageProp@LocalStorageLink 绑定数据:

@Entry(storage)
@Component
struct Index {
  @LocalStorageLink('theme') theme: string = '默认颜色'

  build() {
    Column() {
      Text(this.theme)
        .onClick(() => {
          this.theme = 'Orange'  // 双向同步,LocalStorage会更新
        })
    }
  }
}

子组件也能访问同一个 LocalStorage:

@Component
struct ChildComponent {
  @LocalStorageProp('theme') theme: string = '默认颜色'

  build() {
    Text(this.theme)  // 单向绑定,只能读
  }
}

4.3 LocalStorage vs AppStorage 的选择

场景 选谁
全应用共享(如用户登录态、全局主题) AppStorage
需要持久化到磁盘 PersistentStorage
只在同一个 Ability 的页面间共享 LocalStorage
只在组件内部用 @State

LocalStorage 的好处是作用域有限,不会污染全局命名空间。如果你的数据只需要几个页面共享,别往 AppStorage 里塞,用 LocalStorage 更安全。


五、四大方案横评:AppStorage vs PersistentStorage vs LocalStorage vs @State

我把四种方案的核心差异整理成一张表,方便你做技术选型:

维度 @State LocalStorage AppStorage PersistentStorage
作用域 组件内部 页面级(UIAbility内) 全应用 全应用
持久化 是(磁盘)
生命周期 组件销毁即丢失 UIAbility销毁即丢失 应用退出即丢失 应用重启仍在
数据同步 组件内单向 @LocalStorageProp单向 / @LocalStorageLink双向 @StorageProp单向 / @StorageLink双向 与AppStorage双向同步
典型场景 组件内部UI状态 页面内/页面间共享 跨页面全局共享 全局共享+持久化
数据量建议 无限制 无限制 无限制 <2KB
线程安全 UI线程 UI线程 UI线程 UI线程(同步写磁盘)

选型口诀:

  • 只在自己组件里用@State
  • 同 Ability 多页面共享LocalStorage
  • 全应用共享但不需要持久化AppStorage
  • 全应用共享且需要持久化PersistentStorage + AppStorage

在 Demo 的对比指南 Tab 里,我们也做了可视化展示:

Column() {
  Row() {
    Text('方案').fontSize(12).fontWeight(FontWeight.Medium).width('25%')
    Text('作用域').fontSize(12).fontWeight(FontWeight.Medium).width('20%').textAlign(TextAlign.Center)
    Text('持久化').fontSize(12).fontWeight(FontWeight.Medium).width('15%').textAlign(TextAlign.Center)
    Text('适用场景').fontSize(12).fontWeight(FontWeight.Medium).width('40%')
  }.width('100%').padding({ top: 6, bottom: 6 })
  Row() {
    Text('AppStorage').fontSize(11).width('25%').fontColor('#1a73e8')
    Text('全局').fontSize(11).width('20%').textAlign(TextAlign.Center)
    Text('否').fontSize(11).width('15%').textAlign(TextAlign.Center)
    Text('跨页面共享状态').fontSize(11).width('40%')
  }.width('100%').padding({ top: 3, bottom: 3 })
  Row() {
    Text('PersistentStorage').fontSize(11).width('25%').fontColor('#e65100')
    Text('全局').fontSize(11).width('20%').textAlign(TextAlign.Center)
    Text('是').fontSize(11).width('15%').textAlign(TextAlign.Center).fontColor('#34a853')
    Text('持久化配置/用户偏好').fontSize(11).width('40%')
  }.width('100%').padding({ top: 3, bottom: 3 })
  Row() {
    Text('LocalStorage').fontSize(11).width('25%').fontColor('#34a853')
    Text('页面级').fontSize(11).width('20%').textAlign(TextAlign.Center)
    Text('否').fontSize(11).width('15%').textAlign(TextAlign.Center)
    Text('页面内跨组件共享').fontSize(11).width('40%')
  }.width('100%').padding({ top: 3, bottom: 3 })
  Row() {
    Text('@State').fontSize(11).width('25%').fontColor('#9c27b0')
    Text('组件级').fontSize(11).width('20%').textAlign(TextAlign.Center)
    Text('否').fontSize(11).width('15%').textAlign(TextAlign.Center)
    Text('组件内部状态').fontSize(11).width('40%')
  }.width('100%').padding({ top: 3, bottom: 3 })
}

六、踩坑实录:这些坑我一个没落下

坑1:@StorageLink key 不存在直接崩溃(最常见、最致命)

这是我踩的第一个坑,也是最容易踩的。如果你写了:

@StorageLink('someKey') someValue: number = 0;

someKey 在 AppStorage 里从来没被 setOrCreate 过,应用启动直接崩。不是给你用默认值 0,是直接白屏。报错信息大概是 “AppStorage: key ‘someKey’ does not exist” 之类的,但在真机上有时候只看到白屏,日志里翻半天才能找到。

解决方案:在组件使用之前,必须确保所有 @StorageLink / @StorageProp 绑定的 key 都已经通过 AppStorage.setOrCreate 初始化。最佳实践是在 aboutToAppear 里统一初始化,或者更早——在 EntryAbility 的 onCreate 里。

在我们的 Demo 里,aboutToAppear 做了完整的初始化:

aboutToAppear(): void {
  AppStorage.setOrCreate('appCounter', 0);
  AppStorage.setOrCreate('userName', '游客');
  AppStorage.setOrCreate('userRole', '普通用户');
  AppStorage.setOrCreate('userLevel', 1);
  AppStorage.setOrCreate('themeColor', '#1a73e8');
  AppStorage.setOrCreate('readonlyFlag', false);
}

这里有个小技巧:如果你不确定某个 key 是否已经初始化,可以先用 AppStorage.get('key') 检查,不存在再 setOrCreate。但与其每次都检查,不如统一在一个地方做初始化,代码更清晰。

坑2:@StorageProp 本地修改被 AppStorage 更新覆盖

这个坑非常隐蔽,因为你改了本地值后 UI 确实会变,你以为"已经同步了",但其实只是本地副本变了,AppStorage 那边没动。然后某个时刻 AppStorage 被其他地方更新了,你本地的值突然就被覆盖了,一脸懵逼。

在我们 Demo 里有个专门的演示区域:

@StorageProp('readonlyFlag') readonlyFlag: boolean = false;

// 本地修改 —— AppStorage不变
Button('切换(仅本地)')
  .onClick(() => {
    this.readonlyFlag = !this.readonlyFlag;
    // UI变了,但AppStorage里的readonlyFlag还是false
  })

// AppStorage修改 —— 会覆盖本地值
Button('通过AppStorage改')
  .onClick(() => {
    AppStorage.set('readonlyFlag', !(AppStorage.get<boolean>('readonlyFlag') ?? false));
    // 此时AppStorage的新值会覆盖本地修改
  })

如果你先点了"切换(仅本地)"把本地值改成 true,然后另一个组件通过 AppStorage.set 把 readonlyFlag 设成 false,你的本地 true 会被强制覆盖成 false。这不是 bug,这是 @StorageProp 的设计——AppStorage 是唯一数据源,组件只是它的消费者。

教训:如果你需要修改值并同步到全局,老老实实用 @StorageLink。@StorageProp 就该只用来展示,别想着"我先本地改了,之后某时刻再同步回去"这种骚操作。

坑3:复杂对象在 AppStorage 中的引用问题

AppStorage 的同步机制基于引用变化检测。什么意思呢?对于简单类型(number、string、boolean),赋值就改变了引用,框架能检测到变化并通知组件更新。但对于复杂对象(object、数组),如果你只修改了对象的某个属性,引用地址没变,框架检测不到变化,组件不会刷新。

// 错误:直接修改对象属性,AppStorage检测不到变化
interface UserInfo {
  name: string;
  role: string;
  level: number;
}

let user = AppStorage.get<UserInfo>('userInfo');
user.name = '新名字';  // 引用没变,UI不刷新

正确做法是创建新对象,让引用地址发生变化:

// 正确:创建新对象,引用变化,AppStorage能检测到
let oldUser = AppStorage.get<UserInfo>('userInfo');
let newUser: UserInfo = { name: '新名字', role: oldUser.role, level: oldUser.level };
AppStorage.set('userInfo', newUser);  // 引用变了,UI刷新

或者你也可以用展开运算符:

let oldUser = AppStorage.get<UserInfo>('userInfo');
AppStorage.set('userInfo', { ...oldUser, name: '新名字' });

这个坑和 @State 的对象更新问题是同一套逻辑——ArkUI 的响应式系统只追踪引用变化,不追踪对象内部属性的变化。如果你在 AppStorage 里存复杂对象,一定要记住"改属性不生效,换引用才生效"。

坑4:PersistentStorage 和 AppStorage 同名 key 冲突

前面已经讲过了,这里再强调一遍因为太重要。如果你同时用 AppStorage.setOrCreatePersistentStorage.persistProp 初始化同一个 key,且默认值不同,应用启动时的行为取决于谁后执行——后执行的会覆盖先执行的值。更恶心的是,这个顺序可能在 debug 模式和 release 模式下还不一样,你本地跑着没问题,上线就出幺蛾子。

铁律:一个 key 只让一个"主人"管。需要持久化的 key 让 PersistentStorage 管,不需要持久化的 key 让 AppStorage 管,别混着来。

坑5:PersistentStorage 的时机问题

PersistentStorage 必须在 UI 实例初始化成功后才能调用。如果你在 EntryAbility 的 onCreate 里太早调用,持久化会静默失败——不报错,但数据也不会写到磁盘。正确的时机是在 loadContent 的回调之后。

另外,PersistentStorage 的写磁盘是同步操作,跑在 UI 线程上。如果你频繁修改一个被 persist 的 key,每次修改都会触发一次磁盘写入,UI 直接卡成PPT。所以别把高频变化的数据(比如实时计数器、动画进度)放进 PersistentStorage,只放那种"用户偶尔改一次"的配置项。


七、完整代码走读:GlobalStateDemo

我们的 Demo 页面 GlobalStateDemo 包含两个 Tab:实战演示对比指南。实战演示 Tab 展示了 AppStorage + @StorageLink + @StorageProp 的完整用法,对比指南 Tab 展示了四种方案的横向对比和常见坑提醒。

7.1 页面结构

@Entry
@Component
struct GlobalStateDemo {
  @StorageLink('appCounter') appCounter: number = 0;
  @StorageLink('userName') userName: string = '游客';
  @StorageLink('userRole') userRole: string = '普通用户';
  @StorageLink('userLevel') userLevel: number = 1;
  @StorageLink('themeColor') themeColor: string = '#1a73e8';
  @StorageProp('readonlyFlag') readonlyFlag: boolean = false;
  @State localInput: string = '';
  @State logText: string = '';
  @State activeTab: number = 0;
  // ...
}

注意这里的装饰器分布:

  • appCounteruserNameuserRoleuserLevelthemeColor 用 @StorageLink,因为这些值需要双向同步
  • readonlyFlag 用 @StorageProp,专门用来演示单向绑定的行为
  • localInputlogTextactiveTab 用 @State,因为这些是纯组件内部状态,不需要跨组件共享

7.2 初始化时机

aboutToAppear(): void {
  AppStorage.setOrCreate('appCounter', 0);
  AppStorage.setOrCreate('userName', '游客');
  AppStorage.setOrCreate('userRole', '普通用户');
  AppStorage.setOrCreate('userLevel', 1);
  AppStorage.setOrCreate('themeColor', '#1a73e8');
  AppStorage.setOrCreate('readonlyFlag', false);
}

所有 @StorageLink 和 @StorageProp 使用的 key 都在 aboutToAppear 里统一初始化。这样保证了组件渲染时 AppStorage 里一定有对应的 key,不会崩溃。

有人可能会问:"@StorageLink 声明时不是给了默认值吗?比如 @StorageLink('appCounter') appCounter: number = 0,这个默认值不够用吗?"答案是:不够。这个默认值只是类型声明的一部分,它不会自动帮你往 AppStorage 里创建 key。如果 AppStorage 里没有这个 key,@StorageLink 绑定时直接报错崩溃。默认值只在 key 已存在但类型不匹配等少数场景下作为兜底,绝对不能依赖它来初始化 AppStorage。

7.3 双向同步演示

用户信息更新是典型的 @StorageLink 双向同步场景:

TextInput({ text: this.localInput, placeholder: '修改用户名' })
  .onChange((v: string) => { this.localInput = v; })

Button('更新用户信息')
  .onClick(() => {
    if (this.localInput.length > 0) {
      this.userName = this.localInput;        // 写回AppStorage
      this.userRole = '注册用户';              // 写回AppStorage
      this.userLevel = this.userLevel + 1;    // 写回AppStorage
      this.localInput = '';
      this.addLog('用户信息已更新: name=' + this.userName);
    }
  })

点击按钮后,userNameuserRoleuserLevel 三个值全部通过 @StorageLink 自动同步到 AppStorage。如果有另一个页面也绑了这三个 key,那边会自动刷新。

7.4 主题色全局联动

主题色是最能体现 AppStorage 全局联动效果的场景。色块点击后修改 this.themeColor,页面上所有使用 this.themeColor 渲染的元素(按钮背景、文字颜色、等级进度条、选中边框)全部实时跟随变化:

ForEach(['#1a73e8', '#e65100', '#34a853', '#9c27b0', '#d32f2f'], (color: string) => {
  Row()
    .width(36).height(36).borderRadius(18)
    .backgroundColor(color)
    .border({ width: this.themeColor === color ? 3 : 0, color: '#333333' })
    .onClick(() => {
      this.themeColor = color;
    })
})

这里的 this.themeColor 变化后,其他使用 this.themeColor 的地方(比如 + 按钮的背景色、等级进度条的填充色)都会自动刷新,不需要手动通知。这就是响应式框架的核心优势——你只管改数据,UI 刷新是框架的事。

7.5 单向绑定行为演示

@StorageProp 的行为演示是 Demo 里最有教育意义的部分:

Column() {
  Text('readonlyFlag: ' + this.readonlyFlag.toString())
  Text('(@StorageProp只能读取AppStorage,修改不会回写)')
  Button('切换(仅本地)')
    .onClick(() => {
      this.readonlyFlag = !this.readonlyFlag;
      this.addLog('StorageProp本地切换: ' + this.readonlyFlag.toString() + ' (AppStorage未变)');
    })
  Button('通过AppStorage改')
    .onClick(() => {
      AppStorage.set('readonlyFlag', !(AppStorage.get<boolean>('readonlyFlag') ?? false));
      this.addLog('AppStorage.set readonlyFlag: ' + String(AppStorage.get<boolean>('readonlyFlag')));
    })
}

操作步骤:

  1. 点击"切换(仅本地)",UI 显示 readonlyFlag 变成 true
  2. 点击"通过AppStorage改",AppStorage 把 readonlyFlag 设成 true(因为 AppStorage 里还是 false,取反后变成 true)
  3. 此时 UI 显示的还是 true,但这个 true 是 AppStorage 推过来的,不是你本地的那个

反过来操作更有意思:

  1. 点击"切换(仅本地)",本地值变成 true

  2. 再次点击"通过AppStorage改",AppStorage 把 readonlyFlag 从 false 设成 true

  3. 看起来值没变?但其实你的本地值被 AppStorage 的值覆盖了

  4. 再点一次"通过AppStorage改",AppStorage 从 true 设成 false

  5. 你本地的 true 立刻被覆盖成 false

这就是 @StorageProp 的"AppStorage 变化覆盖本地"行为。在实际开发中,如果你用了 @StorageProp 但又在组件里修改了本地值,一定要意识到本地值随时可能被 AppStorage 的更新覆盖,不要依赖本地值的持久性。


八、架构建议:怎么组织你的全局状态

最后给一些实践层面的建议,不是官方规范,是我自己的经验总结。

8.1 统一初始化

别在各个组件里零散地 setOrCreate,搞一个统一的地方做初始化。推荐在 EntryAbility 的 onCreate 里做全局初始化,或者搞一个专门的 StateInit 工具类:

// StateInit.ets
export function initGlobalState(): void {
  AppStorage.setOrCreate('appCounter', 0);
  AppStorage.setOrCreate('userName', '游客');
  AppStorage.setOrCreate('userRole', '普通用户');
  AppStorage.setOrCreate('userLevel', 1);
  AppStorage.setOrCreate('themeColor', '#1a73e8');
  AppStorage.setOrCreate('readonlyFlag', false);
}

然后在 EntryAbility 里调一次:

onCreate(want, launchParam): void {
  initGlobalState();
}

这样所有 key 的初始化逻辑集中在一个地方,方便审查和维护。以后要加新 key,只改这一个文件就行。

8.2 key 的命名规范

AppStorage 是全局共享的,key 名冲突是迟早的事。建议按模块/功能加前缀:

user_name      → 用户模块
setting_theme  → 设置模块
cart_count     → 购物车模块

别用 namecountcolor 这种太通用的名字,不同模块很容易撞车。

8.3 不要把 AppStorage 当数据库用

AppStorage 是 UI 状态存储,不是通用数据存储。业务数据(商品列表、消息记录、聊天记录)不要往 AppStorage 里塞,它没有查询、排序、分页能力,也不做数据校验。业务数据该用关系型数据库就用关系型数据库,该用网络请求就用网络请求。AppStorage 只放那些"影响 UI 渲染的全局状态"。

8.4 持久化数据和非持久化数据分开管理

需要持久化的 key 和不需要持久化的 key 要清晰区分。建议在代码里用注释或命名区分:

// 持久化key —— 只通过PersistentStorage管理
PersistentStorage.persistProp('setting_themeColor', '#1a73e8');
PersistentStorage.persistProp('setting_fontSize', 14);

// 非持久化key —— 只通过AppStorage管理
AppStorage.setOrCreate('appCounter', 0);
AppStorage.setOrCreate('isRefreshing', false);

两者不要有交集,避免同名 key 冲突。


九、总结

AppStorage 是 HarmonyOS NEXT 全局状态管理的核心机制,配合 @StorageLink 和 @StorageProp 实现跨组件、跨页面的数据同步。PersistentStorage 在 AppStorage 基础上加了磁盘持久化,LocalStorage 提供了更轻量的页面级共享方案。

核心要点:

  1. @StorageLink key 必须先 setOrCreate,否则直接崩溃
  2. @StorageLink 是双向同步,@StorageProp 是单向只读,选错装饰器会导致数据不同步
  3. @StorageProp 本地修改会被 AppStorage 更新覆盖,别拿它当可写变量用
  4. 复杂对象要换引用才能触发更新,改属性不生效
  5. PersistentStorage 和 AppStorage 同名 key 会冲突,一个 key 只让一个主人管
  6. PersistentStorage 写磁盘是同步的,别存大数据,别存高频变化的数据
  7. LocalStorage 适合页面级共享,不需要全局的数据别往 AppStorage 塞

把这些坑记住了,全局状态管理就不会再让你通宵了。至少因为这个通宵,是不会了。


Logo

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

更多推荐