鸿蒙 ArkUI 深水区:Stage 模型多维状态,从「组件内」跳到「应用级」的分水岭

鸿蒙 ArkUI 深水区:Stage 模型多维状态,从「组件内」跳到「应用级」的分水岭

写在前面

如果你写 ArkUI 写过五个组件以上,大概率遇到过这个场景:

用户在「设置页」调了字号,你把字号存进 @State。跳到「首页」,字号回默认了------@State 是组件内状态,跨页面就没了。 你咬牙把字号挪到 @StorageLink + AppStorage,首页能同步了。但用户退出应用再回来,字号又回默认了------AppStorage 是内存级,冷启动就没了。 你再咬牙挂上 PersistentStorage.persistProp,字号终于冷启动还在了。但「护眼模式」开关要只在当前页面 + 子组件共享,不要全局,你又懵了------AppStorage 太大,@State 太小,中间那一层用啥?

这是「组件内状态」和「应用级状态」的分水岭。鸿蒙 6.1 给的答案是 Stage 模型的四级状态容器 ------@State/@Link(组件内)、LocalStorage(页面级共享)、AppStorage(应用级)、PersistentStorage(磁盘持久化),一个不落。

本文就用一个真机可跑的「阅读偏好」demo,把四级状态从「听名字一脸懵」讲到「下个项目直接抄」。代码托管在 AtomGit,文末有链接,真机实拍截图作证。

适合人群:写过 ArkUI、被 @State 跨页面丢状态折磨过的同学。 不适合人群:还在学 @Builder/@Styles 的同学------出门左转看我的上两篇。


一、先讲清楚:Stage 模型到底在管什么

一句话:Stage 模型是鸿蒙的状态管理架构,管着「状态活在哪个范围」「状态活多久」「谁改了状态谁响应」。

你之前写 @State 是组件内状态------只本组件能读写,组件销毁就没了。Stage 模型把这个能力扩成四级:

容器 范围 生命周期 一句话理解
@State/@Link 组件内 组件销毁就没了 「只本组件用」
LocalStorage 页面级共享 页面存活期 「本页 + 子组件共享,不跨页」
AppStorage 应用级 应用存活期 「跨页面全局共享」
PersistentStorage �磁盘持久化 永久(写磁盘) 「冷启动后还在」

四个容器层层递进,从「组件内」到「磁盘」,覆盖所有状态生命周期场景。记住这四级,往下看。


二、动手:一个「阅读偏好」demo 同台演示四级状态

demo 场景:字号缩放、主题色切换、护眼开关------三个偏好分别用不同容器演示。

2.1 数据模型

typescript 复制代码
class ReadPrefs {
  fontScale: number = 1.0
  darkMode: boolean = false
  eyeCare: boolean = false
  ReadPrefs() {}
}

这里用 class 不用 interface 字面量------ArkTS 强约束 arkts-no-untyped-obj-literals,裸对象字面量编译报错,这是新手第一坑。

2.2 主页面:AppStorage + PersistentStorage

typescript 复制代码
@Entry
@Component
struct Index {
  // ① AppStorage 应用级状态:用 @StorageLink 双向同步到 AppStorage 的 key
  @StorageLink('fontScale') fontScale: number = 1.0
  @StorageLink('darkMode') darkMode: boolean = false

  // ② LocalStorage 页面级共享:护眼开关用 LocalStorage 演示
  //    ArkTS 不允许裸对象字面量,必须先声明 Record 再传
  private storageInit: Record<string, boolean> = { 'eyeCare': false }
  private storage: LocalStorage = new LocalStorage(this.storageInit)

  aboutToAppear(): void {
    // AppStorage 先注册 key(@StorageLink 才能链上)
    AppStorage.setOrCreate('fontScale', 1.0)
    AppStorage.setOrCreate('darkMode', false)

    // PersistentStorage 把 AppStorage 的 key 持久化到磁盘
    //    冷启动后这两个 key 会自动从磁盘恢复回 AppStorage
    PersistentStorage.persistProp('fontScale', 1.0)
    PersistentStorage.persistProp('darkMode', false)
  }

  build() {
    Column({ space: 14 }) {
      Text('StageMode 多维状态 Demo').fontSize(22).fontWeight(FontWeight.Bold).margin({ top: 16 })

      // ① AppStorage 联动:字号缩放直接绑到 Text 的 fontSize
      Column({ space: 10 }) {
        Text('① AppStorage:字号实时联动').fontSize(14).fontColor('#007DFF')
        Text('当前字号 x' + this.fontScale.toFixed(1))
          .fontSize(16 * this.fontScale).fontWeight(FontWeight.Bold).fontColor('#222')

        Row({ space: 16 }) {
          Button('A-').width(50).onClick(() => {
            if (this.fontScale > 0.6) {
              // 改 @StorageLink 字段 = 改 AppStorage 的 key = 所有 @StorageLink 都同步
              this.fontScale = Math.round((this.fontScale - 0.2) * 10) / 10
            }
          })
          Button('A+').width(50).onClick(() => {
            if (this.fontScale < 2.0) {
              this.fontScale = Math.round((this.fontScale + 0.2) * 10) / 10
            }
          })
          Button('还原').width(70).onClick(() => { this.fontScale = 1.0 })
        }
      }
      .width('100%').padding(14).backgroundColor('#fff').borderRadius(10)

      // ② AppStorage 联动:主题色,darkMode 切一次整个页面背景跟着变
      Column({ space: 10 }) {
        Text('② AppStorage:主题色实时联动').fontSize(14).fontColor('#007DFF')
        Row({ space: 12 }) {
          Text(this.darkMode ? '🌙 夜间模式' : '☀️ 日间模式')
            .fontSize(16).fontWeight(FontWeight.Bold)
            .fontColor(this.darkMode ? '#fff' : '#222')
          Toggle({ type: ToggleType.Switch, isOn: this.darkMode })
            .onChange((on: boolean) => { this.darkMode = on })
        }
        .width('100%').justifyContent(FlexAlign.SpaceBetween)
      }
      .width('100%').padding(14).backgroundColor(this.darkMode ? '#2A2A2A' : '#fff').borderRadius(10)

      // ③ LocalStorage 页面级:护眼开关,通过子组件 @LocalStorageLink 联动
      Column({ space: 10 }) {
        Text('③ LocalStorage:页面级共享').fontSize(14).fontColor('#007DFF')
        EyeCareToggle({ storage: this.storage })
        Text(this.storage.get<boolean>('eyeCare') ? '已开护眼,背景变暖' : '护眼关')
          .fontSize(13).fontColor('#888')
      }
      .width('100%').padding(14).backgroundColor('#fff').borderRadius(10)

      // ④ 冷启动持久化提示
      Column({ space: 6 }) {
        Text('④ PersistentStorage:磁盘持久化').fontSize(14).fontColor('#007DFF')
        Text('字号和主题退出冷启动后还在,亲手退出再回来试').fontSize(12).fontColor('#888')
      }
      .width('100%').padding(14).backgroundColor('#fff').borderRadius(10)
    }
    .padding(16).backgroundColor(this.darkMode ? '#1A1A1A' : '#F5F6F8').height('100%').width('100%')
  }
}

2.3 子组件:LocalStorage 页面级共享

typescript 复制代码
@Component
struct EyeCareToggle {
  storageInit: Record<string, boolean> = { 'eyeCare': false }
  storage: LocalStorage = new LocalStorage(this.storageInit)
  // @LocalStorageLink 链到父级 LocalStorage 的 key,父级改 = 子级改,反之亦然
  @LocalStorageLink('eyeCare') isOn: boolean = false

  build() {
    Row({ space: 12 }) {
      Toggle({ type: ToggleType.Switch, isOn: this.isOn })
        .onChange((on: boolean) => { this.isOn = on })
      Text('护眼模式').fontSize(14).fontColor('#222')
    }
    .width('100%').justifyContent(FlexAlign.SpaceBetween)
  }
}

三、这段代码的四个关键点

typescript 复制代码
@StorageLink('fontScale') fontScale: number = 1.0

@StorageLink 是双向同步------你改 this.fontScale = 改 AppStorage 的 fontScale key = 所有 @StorageLink('fontScale') 都同步。这是跨页面共享的核心机制。

区别:@StorageProp 是单向同步(只读,AppStorage 改通知你,你改不动 AppStorage)。要双向用 @StorageLink,要只读用 @StorageProp

AppStorage.setOrCreate 先注册 key

typescript 复制代码
aboutToAppear(): void {
  AppStorage.setOrCreate('fontScale', 1.0)
  // ...
}

@StorageLink 链的 key 必须先在 AppStorage 注册,没注册就链会失败。setOrCreate 是「有则不动,无则创建」,适合初始化。

PersistentStorage.persistProp 挂磁盘持久化

typescript 复制代码
PersistentStorage.persistProp('fontScale', 1.0)

把 AppStorage 的 key 挂到 PersistentStorage,鸿蒙自动把这个 key 的值写磁盘。下次冷启动,自动从磁盘读回 AppStorage,@StorageLink 链的字段就有了上次退出前的值。

persistProp 的第二参数是「磁盘里没值时的默认值」,首次启动用。

typescript 复制代码
private storageInit: Record<string, boolean> = { 'eyeCare': false }
private storage: LocalStorage = new LocalStorage(this.storageInit)
// ...
EyeCareToggle({ storage: this.storage })

LocalStorage 是页面级容器,父组件持有,通过 props 传给子组件。子组件用 @LocalStorageLink('eyeCare') 链上来,父子双向同步------不跨页面,只本页 + 子组件共享

何时用 LocalStorage 而非 AppStorage?状态只本页用 + 要给多个子组件共享,就用 LocalStorage;要跨页面全局就用 AppStorage。粒度选对,不要啥都往 AppStorage 塞。


四、真机实拍:四级状态跑起来长这样

我把这个 demo 装到真机上跑(鸿蒙 6.1.1.125, API 24),下面这张是真机实拍,没有任何 P 图。

整体效果:四个区块依次演示「① AppStorage 字号」「② AppStorage 主题色」「③ LocalStorage 护眼」「④ PersistentStorage 磁盘」:

重点看画面:字号缩放按钮(A-/A+/还原)、主题切换 Toggle(日间/夜间)、护眼 Toggle 都在画面里,四个区块对应四种状态容器,字号和主题冷启动后会从磁盘恢复(亲手退出再回来验证)。


五、四级状态容器对比:啥时候用哪个

新手最容易纠结的问题:四级状态,啥场景用哪级?

容器 范围 持久化 跨页面 何时用
@State/@Link 组件内 只本组件用
LocalStorage 页面级 本页 + 子组件共享
AppStorage 应用级 跨页面全局共享
PersistentStorage 磑盘级 是(挂 AppStorage) 冷启动后还在

一句话决策:只在本组件用 @State,本页子组件共享用 LocalStorage,跨页面全局用 AppStorage,要冷启动还在挂 PersistentStorage


六、常见坑(都是血泪)

症状 解法
@StorageLink 链未注册 key 运行时取默认值,改不动 AppStorage.setOrCreate 先注册 key
裸对象字面量传 LocalStorage 编译报错 arkts-no-untyped-obj-literals 先声明 Record<string, X> 变量再传
@StorageLink@StorageProp 混用 状态改不动(单向) 要双向用 Link,要只读用 Prop
把只本页用的状态塞 AppStorage 全局状态膨胀难管 本页共享用 LocalStorage,粒度选对
persistProp 忘调 冷启动状态没了 持久化的 key 必须 PersistentStorage.persistProp
LocalStorage 期望跨页面 跨页面同步失败 跨页面用 AppStorage,LocalStorage 只本页
持久化大数据对象 启动变慢(磁盘读 JSON) 只持久化小键值,大对象用文件 IO

七、完整代码仓库

本文所有代码都已托管到 AtomGit,欢迎 clone、提 issue、点 star:

🔗 仓库地址 :atomgit.com/JaneConan/a...

仓库包含:

  • 完整的「阅读偏好四级状态」demo 工程
  • Index.ets 主页面(AppStorage + PersistentStorage + LocalStorage 三容器)
  • EyeCareToggle 子组件(@LocalStorageLink 页面级共享)
  • ReadPrefs 数据模型 + ArkTS 裸对象字面量正确写法示范
  • 可直接用 DevEco Studio 打开运行

八、下一步该学什么?

跑通这个 demo 之后,你的 ArkUI 状态管理就齐了四级容器。这是 ArkUI 系列的收尾篇------下一篇我们要从 UI 转向非 UI 了。建议按这个顺序往下:

  1. @Watch 状态监听 :状态变了自动跑一段逻辑,比 onChange 更声明式
  2. @Observed/@ObjectLink 深层观察:嵌套对象的属性变化能触发重绘
  3. Provide/Consume 跨层传递:替代 props 层层透传,祖先直接给后代
  4. (转非 UI)HTTP 数据请求 :@ohos.net.http 调 RESTful 接口,告别前端 fetch
  5. (转非 UI)文件 IO :@ohos.file.fs 读写沙箱文件,大对象持久化正确姿势
  6. (转非 UI)能力调用 :@ohos.ability 调起相机、相册、定位等系统能力

写在最后

Stage 模型四级状态,本质是**「状态的生命周期分层」**------组件内、页面级、应用级、磁盘级,每层覆盖不同生命周期需求。这个思想在前端圈叫 React Context/Redux/Persist,在鸿蒙圈叫 @State/LocalStorage/AppStorage/PersistentStorage,名字不同灵魂相通。

一旦你开始用状态分层思维写应用,你会发现大部分状态需求都能对应到某一层容器------跨页面的不丢、冷启动的不丢、本页的不外溢,代码量少一半,bug 少九成。

代码已经给你了,仓库链接在上面。现在关掉这篇文章,打开 DevEco Studio,把 demo 跑起来,亲手调字号再退出再回来,验证 PersistentStorage 冷启动还在。

跑通了,回来评论区打个「1」,我看看有多少人真的动手了。🚀


作者:JaneConan 仓库:atomgit.com/JaneConan/a... 协议:Apache-2.0,随便用,别告我

相关推荐
长栎17 小时前
用AI优化Redis缓存失效,我把热key问题想简单了
后端
面试鸭17 小时前
我说我们做 Agent 从来不怕死循环,面试官调出兜底日志:“第 47 次强制终止,是你半夜爬起来按停的?”
后端·面试·求职
小小猪的春天17 小时前
团队级 Code Review Skill 实践:4个人、4个AI、1套规则,CR从找bug变成做决策
后端·架构
长栎17 小时前
AI帮我写了个Spring Boot校验,线上漏掉了这组边界条件
后端
外滩运维专家17 小时前
Let's Encrypt 的 ACME 协议到底干了什么?用抓包带你看一遍
后端
程序员多吃鸭eatmoreduck18 小时前
# 开源初体验,39 天 800 Star:我把项目发到了哪里,流量到底从哪来
后端
雪隐19 小时前
个人电脑玩AI-10让5060 Ti给你打工——我让 Claude Code 喝上了本地杂粮:Ternary-Bonsai-27B 部署历险记
前端·人工智能·后端
云上小朱19 小时前
Intel SGX相关软件部署
后端