鸿蒙 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)
}
}
三、这段代码的四个关键点
① @StorageLink 双向同步到 AppStorage
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的第二参数是「磁盘里没值时的默认值」,首次启动用。
④ LocalStorage + @LocalStorageLink 页面级
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 了。建议按这个顺序往下:
@Watch状态监听 :状态变了自动跑一段逻辑,比onChange更声明式@Observed/@ObjectLink深层观察:嵌套对象的属性变化能触发重绘Provide/Consume跨层传递:替代 props 层层透传,祖先直接给后代- (转非 UI)HTTP 数据请求 :
@ohos.net.http调 RESTful 接口,告别前端 fetch - (转非 UI)文件 IO :
@ohos.file.fs读写沙箱文件,大对象持久化正确姿势 - (转非 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,随便用,别告我