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

相关推荐
IT_陈寒15 分钟前
Vue的数组更新把我坑惨了
前端·人工智能·后端
高晶20 分钟前
Android 智能语音开发新手入门实战
后端·程序员
励志不掉头发的内向程序员22 分钟前
鼠标点一下,图形是怎么画出来的?拆解 CAD 的 Action 状态机
后端·架构
高晶30 分钟前
Android 机器视觉新手入门:从扫码到人脸实战
后端·程序员
写码跑山的老艾31 分钟前
基于 FSB 快速搭建 AI 客服:专家扩展与 Spring Boot 接入
后端
云边有个稻草人35 分钟前
从语句编写到执行追踪:金仓 SQL 编辑器实战指南
后端
用户2049375549541 分钟前
中文流式识别偶发重复词的定位与修复实践
后端
小蒜学长1 小时前
基于Java的公司采购系统的设计与实现(代码+数据库+LW)
java·数据库·spring boot·后端·公司采购系统
小坏讲微服务2 小时前
Spring Boot 4 新特性全解析:从上手到生产实战
java·spring boot·后端·架构·springboot4
huaweichenai4 小时前
spring boot 实现file文件上传
java·spring boot·后端