HarmonyOS 应用开发《掌上英语》第5篇:依赖注入与全局状态:AppStorageV2 实战 依赖注入与全局状态AppStorageV2 实战一、引言在 HarmonyOS 多模块应用中跨模块状态共享是一个核心挑战。传统方案如 EventBus 存在类型不安全、调试困难等问题。HarmonyOS 从 API 12 开始引入的AppStorageV2提供了一种类型安全、可观察的全局状态管理方案。本文基于一个 11 模块的英语学习 App深入分析 AppStorageV2 的四种典型使用场景页面组件中的全局状态连接、工具类中的状态注入、基类抽象层的统一管理以及单例模式与 AppStorageV2 的结合使用。二、AppStorageV2 的基础用法AppStorageV2 是 HarmonyOS 的全局键-值存储系统其核心 API 是connect方法import{AppStorageV2}fromkit.ArkUI;ObservedV2classMyModel{Tracevalue:stringdefault;}// 连接全局存储如果键不存在则使用工厂函数初始化letinstance:MyModelAppStorageV2.connect(MyModel,()newMyModel())!;connect的行为规则如下键已存在返回 AppStorageV2 中已存储的实例跨模块共享同一个实例键不存在调用工厂函数创建新实例并存储到 AppStorageV2 中返回值使用!非空断言因为返回值可能为undefined这种设计天然支持了单实例、跨模块共享的语义。三、场景一页面组件的全局状态连接在页面组件中最常见的用法是通过Local装饰器配合AppStorageV2.connect获取全局状态。3.1 用户信息全局共享在MinePage我的页面中用户信息通过 AppStorageV2 全局连接import{AppStorageV2}fromkit.ArkUI;import{UserInfo}fromlogin_info;ComponentV2exportstruct MinePage{LocallogoUser:UserInfoAppStorageV2.connect(UserInfo,()newUserInfo())!;// ...}这里的UserInfo是一个被ObservedV2装饰的类其属性使用Trace标记以实现可观察性// components/login_info/src/main/ets/model/UserInfo.etsObservedV2exportclassUserInfo{Traceid:number0;Traceavatar:ResourceStr;Tracename:string;Tracecellphone:string;Traceaddress:string;TraceisLogin:booleanfalse;}当UserInfo的属性如isLogin、name发生变化时所有连接到该实例的组件会自动刷新。3.2 跨页面同步更新有意思的是UserInfo的写入操作发生在LoginPage中// product/entry/src/main/ets/pages/LoginPage.etsimport{QuickLogin,UserInfo}fromlogin_info;ComponentV2exportstruct LoginPage{build(){QuickLogin({onLoginWithHuaweiID:(flag:boolean,phone?:string){letprop:UserInfoAppStorageV2.connect(UserInfo,()newUserInfo())!;if(!prop.name){prop.name华为用户;}prop.isLogintrue;prop.id0;prop.cellphonephone??177******96;prop.addressxxx省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.etsObservedV2exportclassSample{Traceid:string1111;Tracetitle:string英语词汇速记;}在页面中使用Localprop:SampleAppStorageV2.connect(Sample,()newSample())!;四、场景二工具类中的状态注入AppStorageV2 不仅可以在组件中使用还可以在纯 TypeScript 类中使用。BreakpointUtils是一个典型例子// commons/commonLib/src/main/ets/utils/BreakpointUtils.etsimport{AppStorageV2,window}fromkit.ArkUI;import{BreakpointModel,BreakpointNameEnum}from../models/BreakpointModel;exportclassBreakpointUtils{publicuiContext?:UIContext;publicmainWindow?:window.Window;privatebreakpointModel:BreakpointModelnewBreakpointModel();constructor(mainWindow:window.Window){try{this.mainWindowmainWindow;this.uiContextmainWindow.getUIContext();this.breakpointModelAppStorageV2.connect(BreakpointModel,()newBreakpointModel())!;}catch(e){// 异常处理}}// ...}关键设计要点1. 在构造函数中初始化BreakpointUtils在构造函数中调用AppStorageV2.connect确保在工具类的生命周期起点就绑定到全局状态。2. 覆盖本地实例构造函数中先创建this.breakpointModel new BreakpointModel()作为本地默认值然后立即用AppStorageV2.connect返回值覆盖。这种先默认再覆盖的策略确保了即使 AppStorageV2 连接失败实例也不会为undefined。3. 监听系统断点变化并更新全局状态register(){consthandleBreakpoint(widthBp:WidthBreakpoint){this.updateBreakpoint(widthBp);};// 初始化赋值handleBreakpoint(this.uiContext!.getWindowWidthBreakpoint());// 窗口变化时更新this.mainWindow!.on(windowSizeChange,(){handleBreakpoint(this.uiContext!.getWindowWidthBreakpoint());});}privateupdateBreakpoint:(value:number)void(value:number){letbreakPointthis.transformBreakpointNameEnum(value);this.breakpointModel.currentBreakpointbreakPointasBreakpointNameEnum;};当系统窗口尺寸变化时BreakpointUtils更新breakpointModel.currentBreakpoint所有通过AppStorageV2.connect(BreakpointModel, ...)连接到该实例的组件会自动感知变化并重新渲染。五、场景三基类抽象层的统一管理为了简化 ViewModel 中的重复代码项目设计了BaseViewModel基类// commons/commonLib/src/main/ets/models/BaseViewModel.etsimport{AppStorageV2}fromkit.ArkUI;import{BreakpointModel}from./BreakpointModel;ObservedV2exportabstractclassBaseViewModel{TracebreakPointModel:BreakpointModelAppStorageV2.connect(BreakpointModel,()newBreakpointModel())!;}所有 ViewModel 继承自此基类// features/minePage/src/main/ets/viewModel/MineVM.etsimport{BaseViewModel}fromcommonlib;ObservedV2exportclassMineVMextendsBaseViewModel{privatestatic_instance:MineVM;publicstaticgetinstance(){if(!MineVM._instance){MineVM._instancenewMineVM();}returnMineVM._instance;}}这种设计带来了几个好处1. 断点响应开箱即用每个 ViewModel 实例自动拥有breakPointModel属性无需手动连接。2. Trace 确保响应式基类中Trace breakPointModel确保所有继承者都能感知断点变化。3. 单例 继承的组合模式子类使用单例模式MineVM.instance而断点状态通过继承自动获得两层模式各司其职。在页面中使用断点信息// MinePage 中使用.columnsTemplate(1fr .repeat(newBreakpointTypenumber({sm:1,md:1,lg:2,xl:2}).getValue(this.mineVM.breakPointModel.currentBreakpoint)))六、场景四单例模式与 AppStorageV2 的结合MineVM展示了单例模式与 AppStorageV2 的经典组合ObservedV2exportclassMineVMextendsBaseViewModel{privatestatic_instance:MineVM;publicstaticgetinstance(){if(!MineVM._instance){MineVM._instancenewMineVM();}returnMineVM._instance;}}而在页面中使用时ComponentV2struct MinePage{vm:MineVMMineVM.instance;// ...}此时MineVM是单例的而它继承的breakPointModel是通过AppStorageV2.connect获取的全局单例。形成了两级单例架构ViewModel 级单例MineVM.instance确保全局只有一个 ViewModel 实例AppStorageV2 级单例BreakpointModel通过 AppStorageV2 确保全局唯一这种设计避免了重复创建带来的资源浪费同时保证了状态一致性。七、ObservedV2 Trace vs AppStorageV2 的选择策略在实际开发中需要根据场景选择合适的方案场景推荐方案原因组件内部状态Local作用域仅限当前组件无需暴露到全局父子组件传参Param/Event显式的数据流符合单向数据流原则兄弟组件共享AppStorageV2不需要中间组件中转跨模块共享AppStorageV2唯一的跨模块方案ViewModel 内部状态ObservedV2Trace仅在当前类内部使用无需全局可见单例 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 和 Preferencesprop.isLogintrue;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.etsLoginPage 页面d:\HarmonyOS\WorkSpace\Exam1.0.4\product\entry\src\main\ets\pages\LoginPage.etsBreakpointUtilsd:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\utils\BreakpointUtils.etsBreakpointModeld:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\models\BreakpointModel.etsBaseViewModeld:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\models\BaseViewModel.etsMineVMd:\HarmonyOS\WorkSpace\Exam1.0.4\features\minePage\src\main\ets\viewModel\MineVM.etsUserInfo 模型d:\HarmonyOS\WorkSpace\Exam1.0.4\components\login_info\src\main\ets\model\UserInfo.etsSample 模型d:\HarmonyOS\WorkSpace\Exam1.0.4\commons\commonLib\src\main\ets\models\SampleModel.ets