鸿蒙 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,随便用,别告我

相关推荐
天天喝旺仔5 分钟前
Docker 镜像瘦身实战:多阶段构建把体积缩小 90%
运维·后端·ci/cd·docker·云原生·容器·性能优化
Lost of 程序猿10 分钟前
ASP.NET Core Saga 分布式事务深度实战:备件采购跨服务长流程,如何保证“要么全成,要么全回“
分布式·后端·asp.net
卷无止境12 分钟前
Coding Agent 里的上下文 Compact,到底在压缩什么
后端·python
爱勇宝19 分钟前
公司没给活干,却因为员工看手机把人开了:法院判赔11万元
前端·后端·程序员
2601_9620710023 分钟前
【Java EE】SpringBoot的创建与简单使用
spring boot·后端·java-ee
Csvn25 分钟前
🐍 Day 7:文件 I/O 实战
后端
摇滚侠40 分钟前
《SpringBoot 3:入门与应用实战》第 9 章 使用 WebMvc 开发进阶 阅读笔记 24
spring boot·笔记·后端
yunwei371 小时前
AgentCgroup:当 AI Agent 遇到操作系统资源
linux·人工智能·后端
元界metalite1 小时前
SpringBoot开发企业后台-操作日志记录的最佳实践
后端
用户298698530141 小时前
PDF 转纯文本(TXT)免费攻略:轻松提取文字内容
人工智能·后端·c#