HarmonyOS 应用开发《掌上英语》第5篇:依赖注入与全局状态:AppStorageV2 实战HarmonyOS 应用开发《掌上英语》第1篇
依赖注入与全局状态:AppStorageV2 实战

一、引言
在 HarmonyOS 多模块应用中,跨模块状态共享是一个核心挑战。传统方案如 EventBus 存在类型不安全、调试困难等问题。HarmonyOS 从 API 12 开始引入的 AppStorageV2 提供了一种类型安全、可观察的全局状态管理方案。
本文基于一个 11 模块的英语学习 App,深入分析 AppStorageV2 的四种典型使用场景:页面组件中的全局状态连接、工具类中的状态注入、基类抽象层的统一管理以及单例模式与 AppStorageV2 的结合使用。
二、AppStorageV2 的基础用法
AppStorageV2 是 HarmonyOS 的全局键-值存储系统,其核心 API 是 connect 方法:
import { AppStorageV2 } from '@kit.ArkUI';
@ObservedV2
class MyModel {
@Trace value: string = 'default';
}
// 连接全局存储,如果键不存在则使用工厂函数初始化
let instance: MyModel = AppStorageV2.connect(MyModel, () => new MyModel())!;
connect 的行为规则如下:
- 键已存在:返回 AppStorageV2 中已存储的实例(跨模块共享同一个实例)
- 键不存在:调用工厂函数创建新实例并存储到 AppStorageV2 中
- 返回值:使用
!非空断言,因为返回值可能为undefined
这种设计天然支持了"单实例、跨模块共享"的语义。
三、场景一:页面组件的全局状态连接
在页面组件中,最常见的用法是通过 @Local 装饰器配合 AppStorageV2.connect 获取全局状态。
3.1 用户信息全局共享
在 MinePage(我的页面)中,用户信息通过 AppStorageV2 全局连接:
import { AppStorageV2 } from '@kit.ArkUI';
import { UserInfo } from 'login_info';
@ComponentV2
export struct MinePage {
@Local logoUser: UserInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())!;
// ...
}
这里的 UserInfo 是一个被 @ObservedV2 装饰的类,其属性使用 @Trace 标记以实现可观察性:
// components/login_info/src/main/ets/model/UserInfo.ets
@ObservedV2
export class UserInfo {
@Trace id: number = 0;
@Trace avatar: ResourceStr = '';
@Trace name: string = '';
@Trace cellphone: string = '';
@Trace address: string = '';
@Trace isLogin: boolean = false;
}
当 UserInfo 的属性(如 isLogin、name)发生变化时,所有连接到该实例的组件会自动刷新。
3.2 跨页面同步更新
有意思的是,UserInfo 的写入操作发生在 LoginPage 中:
// product/entry/src/main/ets/pages/LoginPage.ets
import { QuickLogin, UserInfo } from 'login_info';
@ComponentV2
export struct LoginPage {
build() {
QuickLogin({
onLoginWithHuaweiID: (flag: boolean, phone?: string) => {
let prop: UserInfo = AppStorageV2.connect(UserInfo, () => new UserInfo())!;
if (!prop.name) {
prop.name = '华为用户';
}
prop.isLogin = true;
prop.id = 0;
prop.cellphone = phone ?? '177******96';
prop.address = 'xxx省xxx市';
}
});
}
}
LoginPage 和 MinePage 分别在不同的模块中——LoginPage 在 entry 模块,MinePage 在 features/minePage 模块。但它们通过 AppStorageV2.connect(UserInfo, ...) 连接到了同一个全局实例。当 LoginPage 修改了 UserInfo 的属性后,MinePage 中的 UI 会自动刷新。这就是 AppStorageV2 的跨模块响应式绑定能力。
3.3 示例数据模型
项目中的 Sample 数据模型也使用了相同的模式:
// commons/commonLib/src/main/ets/models/SampleModel.ets
@ObservedV2
export class Sample {
@Trace id: string = '1111';
@Trace title: string = '英语词汇速记';
}
在页面中使用:
@Local prop: Sample = AppStorageV2.connect(Sample, () => new Sample())!;
四、场景二:工具类中的状态注入
AppStorageV2 不仅可以在组件中使用,还可以在纯 TypeScript 类中使用。BreakpointUtils 是一个典型例子:
// commons/commonLib/src/main/ets/utils/BreakpointUtils.ets
import { AppStorageV2, window } from '@kit.ArkUI';
import { BreakpointModel, BreakpointNameEnum } from '../models/BreakpointModel';
export class BreakpointUtils {
public uiContext?: UIContext;
public mainWindow?: window.Window;
private breakpointModel: BreakpointModel = new BreakpointModel();
constructor(mainWindow: window.Window) {
try {
this.mainWindow = mainWindow;
this.uiContext = mainWindow.getUIContext();
this.breakpointModel = AppStorageV2.connect(BreakpointModel, () => new BreakpointModel())!;
} catch (e) {
// 异常处理
}
}
// ...
}
关键设计要点:
1. 在构造函数中初始化BreakpointUtils 在构造函数中调用 AppStorageV2.connect,确保在工具类的生命周期起点就绑定到全局状态。
2. 覆盖本地实例
构造函数中先创建 this.breakpointModel = new BreakpointModel() 作为本地默认值,然后立即用 AppStorageV2.connect 返回值覆盖。这种"先默认再覆盖"的策略确保了即使 AppStorageV2 连接失败,实例也不会为 undefined。
3. 监听系统断点变化并更新全局状态
register() {
const handleBreakpoint = (widthBp: WidthBreakpoint) => {
this.updateBreakpoint(widthBp);
};
// 初始化赋值
handleBreakpoint(this.uiContext!.getWindowWidthBreakpoint());
// 窗口变化时更新
this.mainWindow!.on('windowSizeChange', () => {
handleBreakpoint(this.uiContext!.getWindowWidthBreakpoint());
});
}
private updateBreakpoint: (value: number) => void = (value: number) => {
let breakPoint = this.transformBreakpointNameEnum(value);
this.breakpointModel.currentBreakpoint = breakPoint as BreakpointNameEnum;
};
当系统窗口尺寸变化时,BreakpointUtils 更新 breakpointModel.currentBreakpoint,所有通过 AppStorageV2.connect(BreakpointModel, ...) 连接到该实例的组件会自动感知变化并重新渲染。
五、场景三:基类抽象层的统一管理
为了简化 ViewModel 中的重复代码,项目设计了 BaseViewModel 基类:
// commons/commonLib/src/main/ets/models/BaseViewModel.ets
import { AppStorageV2 } from '@kit.ArkUI';
import { BreakpointModel } from './BreakpointModel';
@ObservedV2
export abstract class BaseViewModel {
@Trace breakPointModel: BreakpointModel = AppStorageV2.connect(
BreakpointModel, () => new BreakpointModel()
)!;
}
所有 ViewModel 继承自此基类:
// features/minePage/src/main/ets/viewModel/MineVM.ets
import { BaseViewModel } from 'commonlib';
@ObservedV2
export class MineVM extends BaseViewModel {
private static _instance: MineVM;
public static get instance() {
if (!MineVM._instance) {
MineVM._instance = new MineVM();
}
return MineVM._instance;
}
}
这种设计带来了几个好处:
1. 断点响应开箱即用
每个 ViewModel 实例自动拥有 breakPointModel 属性,无需手动连接。
2. @Trace 确保响应式
基类中 @Trace breakPointModel 确保所有继承者都能感知断点变化。
3. 单例 + 继承的组合模式
子类使用单例模式 MineVM.instance,而断点状态通过继承自动获得,两层模式各司其职。
在页面中使用断点信息:
// MinePage 中使用
.columnsTemplate('1fr '.repeat(
new BreakpointType<number>({
sm: 1,
md: 1,
lg: 2,
xl: 2
}).getValue(this.mineVM.breakPointModel.currentBreakpoint)
))
六、场景四:单例模式与 AppStorageV2 的结合
MineVM 展示了单例模式与 AppStorageV2 的经典组合:
@ObservedV2
export class MineVM extends BaseViewModel {
private static _instance: MineVM;
public static get instance() {
if (!MineVM._instance) {
MineVM._instance = new MineVM();
}
return MineVM._instance;
}
}
而在页面中使用时:
@ComponentV2
struct MinePage {
vm: MineVM = MineVM.instance;
// ...
}
此时,MineVM 是单例的,而它继承的 breakPointModel 是通过 AppStorageV2.connect 获取的全局单例。形成了"两级单例"架构:
- ViewModel 级单例:
MineVM.instance确保全局只有一个 ViewModel 实例 - AppStorageV2 级单例:
BreakpointModel通过 AppStorageV2 确保全局唯一
这种设计避免了重复创建带来的资源浪费,同时保证了状态一致性。
七、@ObservedV2 + @Trace vs AppStorageV2 的选择策略
在实际开发中,需要根据场景选择合适的方案:
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 组件内部状态 | @Local |
作用域仅限当前组件,无需暴露到全局 |
| 父子组件传参 | @Param / @Event |
显式的数据流,符合单向数据流原则 |
| 兄弟组件共享 | AppStorageV2 |
不需要中间组件中转 |
| 跨模块共享 | AppStorageV2 |
唯一的跨模块方案 |
| ViewModel 内部状态 | @ObservedV2 + @Trace |
仅在当前类内部使用,无需全局可见 |
| 单例 ViewModel 的全局状态 | 单例 + AppStorageV2.connect |
兼顾全局共享和单例管控 |
选择原则:
- 优先使用最局部的作用域:能用
@Local不用@Param,能用@Param不用AppStorageV2 - 跨模块必须用 AppStorageV2:模块间的通信只能通过 AppStorageV2
- 纯逻辑类(非组件)用 AppStorageV2:因为非组件类无法使用
@Local/@Param等装饰器 - 避免滥用:AppStorageV2 中的键越多,生命周期管理越复杂,只存放真正需要全局共享的状态
八、全局状态的生命周期管理
AppStorageV2 的状态生命周期与进程绑定,在以下情况下需要注意:
1. 初始化时机
第一次调用 AppStorageV2.connect(key, factory) 时创建实例,通常放在 Ability 的 onCreate 或第一个页面的 aboutToAppear 中。
2. 不主动销毁
AppStorageV2 中的实例在应用进程退出时自动释放,不需要手动调用删除。
3. 状态持久化
AppStorageV2 是内存级存储,应用被杀后数据丢失。需要持久化的数据(如用户登录状态)应同时存储到 Preferences 中:
// 登录成功后同时写入 AppStorageV2 和 Preferences
prop.isLogin = true;
PreferenceUtil.put('isLogin', true);
4. 跨进程限制
AppStorageV2 仅在同一个 UIAbility 进程中共享,不同的 UIAbility 实例无法共享。
九、最佳实践总结
- 类型安全优先:始终使用
@ObservedV2类作为 AppStorageV2 的存储类型,不要使用原始类型 - 工厂函数确保初始化:
connect的第二个参数提供工厂函数,确保键不存在时能正确初始化 - 基类封装减少重复:像
BaseViewModel一样,将通用的 AppStorageV2 连接封装到基类中 - 配合单例模式使用:ViewModel + AppStorageV2 的组合兼顾了全局共享和实例管理
- 避免在 AppStorageV2 中存储大量数据:只存放跨模块共享的"全局配置"类状态
- 清晰的连接命名:使用类本身作为键名(如
BreakpointModel、UserInfo),而不是字符串字面量
十、参考源码路径
| 文件 | 路径 |
|---|---|
| MinePage 页面 | d:\HarmonyOS\WorkSpace\Exam1.0.4\features\minePage\src\main\ets\views\MinePage.ets |
| LoginPage 页面 | d:\HarmonyOS\WorkSpace\Exam1.0.4\product\entry\src\main\ets\pages\LoginPage.ets |
| BreakpointUtils | d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\utils\BreakpointUtils.ets |
| BreakpointModel | d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\models\BreakpointModel.ets |
| BaseViewModel | d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\models\BaseViewModel.ets |
| MineVM | d:\HarmonyOS\WorkSpace\Exam1.0.4\features\minePage\src\main\ets\viewModel\MineVM.ets |
| UserInfo 模型 | d:\HarmonyOS\WorkSpace\Exam1.0.4\components\login_info\src\main\ets\model\UserInfo.ets |
| Sample 模型 | d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\models\SampleModel.ets |
更多推荐

所有评论(0)