依赖注入与全局状态: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 的属性(如 isLoginname)发生变化时,所有连接到该实例的组件会自动刷新。

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市';
      }
    });
  }
}

LoginPageMinePage 分别在不同的模块中——LoginPageentry 模块,MinePagefeatures/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 兼顾全局共享和单例管控

选择原则

  1. 优先使用最局部的作用域:能用 @Local 不用 @Param,能用 @Param 不用 AppStorageV2
  2. 跨模块必须用 AppStorageV2:模块间的通信只能通过 AppStorageV2
  3. 纯逻辑类(非组件)用 AppStorageV2:因为非组件类无法使用 @Local/@Param 等装饰器
  4. 避免滥用: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 实例无法共享。


九、最佳实践总结

  1. 类型安全优先:始终使用 @ObservedV2 类作为 AppStorageV2 的存储类型,不要使用原始类型
  2. 工厂函数确保初始化connect 的第二个参数提供工厂函数,确保键不存在时能正确初始化
  3. 基类封装减少重复:像 BaseViewModel 一样,将通用的 AppStorageV2 连接封装到基类中
  4. 配合单例模式使用:ViewModel + AppStorageV2 的组合兼顾了全局共享和实例管理
  5. 避免在 AppStorageV2 中存储大量数据:只存放跨模块共享的"全局配置"类状态
  6. 清晰的连接命名:使用类本身作为键名(如 BreakpointModelUserInfo),而不是字符串字面量

十、参考源码路径

文件 路径
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
Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐