页面级状态由组件装饰器管理,而"跨页面、跨组件树、与应用同生命周期"的全局状态需要专门容器:V1 时代的 AppStorage 与 V2 时代的 AppStorageV2。本项目用 AppStorageV2 承载最关键的全局对象——窗口信息 WindowInfo,由 common/multishortvideobase/src/main/ets/utils/WindowUtil.ets 统一维护;同时用 @Provider/@Consumer 在页面根部构建 Provider 树共享路由栈、侧栏开关、深色模式等。本文对比 AppStorage 与 AppStorageV2 的差异,结合项目代码讲解全局状态的最佳实践。
24 — AppStorage/AppStorageV2 应用级状态

二、AppStorage 基础与局限

AppStorage 是 V1 的 UI 应用级状态容器,单例驻留于 UIAbility 生命周期内,支持简单类型与对象。V1 常用写法:

// V1 写法(对照):存储与读取全局属性
AppStorage.setOrCreate('isDark', false);          // 写入
const isDark = AppStorage.get<boolean>('isDark'); // 读取

局限同样明显:对嵌套对象需配合 @Observed/@ObjectLink 才能深层观察;所有操作都走字符串 key,缺少类型约束;装饰器(@StorageLink/@StorageProp)与 V2 组件体系不兼容,项目迁到 V2 后无法直接使用。因此在 V2 工程中,AppStorage 仅作为兼容层存在,新代码应使用 AppStorageV2。

三、AppStorageV2 与 connect 连接

AppStorageV2 以"连接类对象"为核心:用 @ObservedV2 装饰的类作为存储类型,connect 建立连接后返回同一实例,任何持有该实例的组件都能观察其 @Trace 属性。WindowUtil.ets 中的实现:

// common/multishortvideobase/src/main/ets/utils/WindowUtil.ets(真实代码)
import { AppStorageV2, UIContext, window } from '@kit.ArkUI';

export const WINDOW_INFO_STORAGE_KEY = 'MultiShortVideoWindowInfo';

@ObservedV2
export class WindowInfo {
  @Trace public windowStatusType: window.WindowStatusType = window.WindowStatusType.UNDEFINED;
  @Trace public isFullScreen: boolean = false;
  @Trace public orientation: window.Orientation = window.Orientation.UNSPECIFIED;
  @Trace public windowSize: window.Size = { width: 0, height: 0 };
  @Trace public widthBp: WidthBreakpoint = WidthBreakpoint.WIDTH_XS;
  @Trace public heightBp: HeightBreakpoint = HeightBreakpoint.HEIGHT_SM;
  ...
}

export function getWindowInfoState(): WindowInfo {
  return AppStorageV2.connect<WindowInfo>(WindowInfo, WINDOW_INFO_STORAGE_KEY, () => new WindowInfo())!;
}

connect 的三个参数分别是存储类型、存储键、默认值工厂。首次调用创建并存入,之后任何模块调用 getWindowInfoState() 拿到的都是同一实例;由于类成员带 @Trace,属性修改会精确通知所有引用该对象的组件。WindowUtil 单例持有 mainWindowInfo 引用,在窗口回调中就地修改属性:

public onWindowSizeChange: (windowSize: window.Size) => void = (windowSize: window.Size) => {
  this.mainWindowInfo.windowSize = windowSize;
  this.mainWindowInfo.widthBp = this.uiContext!.getWindowWidthBreakpoint();
  this.mainWindowInfo.heightBp = this.uiContext!.getWindowHeightBreakpoint();
};

组件侧只需 @Local windowInfo: WindowInfo = getWindowInfoState(),即可在断点、避让区、沉浸状态变化时自动刷新,无需任何手动通知——这是本项目"断点驱动自适应布局"的状态基石。

四、Provider/Consumer 树:页面级全局状态的分工

AppStorageV2 解决"应用级、跨模块"状态;页面内跨组件通信则交给 @Provider/@Consumer 树,二者分工如下:

状态载体生命周期示例
窗口信息、避让区AppStorageV2 + WindowInfo整个应用widthBp、statusBarTopVp
主题/侧栏/路由@Provider/@Consumer页面/路由栈isDark、showSideComment
播放进度@Provider/@Consumer视频页组件树currentTime、duration

products/default/src/main/ets/view/Index.ets 在根组件构建 Provider 树:

@Entry
@ComponentV2
struct Index {
  @Local windowInfo: WindowInfo = getWindowInfoState();
  @Provider('isDark') isDark: boolean = false;
  @Provider('pathStack') pathStack: NavPathStack = new NavPathStack();
  @Provider('showSideComment') showSideComment: boolean = false;
  @Provider('showSideIndividual') showSideIndividual: boolean = false;
  ...
}

视频页 AdaptiveVideo.ets 与播放器 AdaptiveAVPlayer.ets 则跨层消费/转发(@Consumer('showSideComment')@Consumer('currentTime') 等)。当用户在大屏点击评论图标时,AdaptiveVideo 修改 showSideComment 并 pushPathByName(‘SplitComment’),根组件 Navigation 随即切换为分栏模式;点击空白区域时,根组件的手势判定会 pop 路由并复位侧栏开关——Provider/Consumer 树让"深层组件改、根部响应"成为可能。

值得注意的还有 AdaptiveVideo 的"消费再提供"模式:它从根组件消费 pathStack 与侧栏开关,同时把播放器上报的进度(@Consumer(‘currentTime’)/@Consumer(‘duration’))以 @Provider(‘windowSizeWidth’)/@Provider(‘commentCount’) 等继续下发给更深层的 UI,形成"中转站"。这比让每个深层组件都直接连接 AppStorageV2 更符合"就近提供、按需消费"的原则——AppStorageV2 只存放真正需要全局唯一的对象,页面内部的共享数据留在组件树内,既减少全局命名空间污染,也让数据流的追踪路径保持在页面范围内。

五、AppStorageV2 的更多能力与生命周期

AppStorageV2 除 connect 外,还提供 remove 等管理接口,可用于在页面退出时解除连接:

// 解除指定 key 的连接(AppStorageV2 管理接口)
AppStorageV2.remove<WindowInfo>(WINDOW_INFO_STORAGE_KEY);

需要明确的是:AppStorageV2 的生命周期与应用进程一致,窗口信息这类"从 Ability 启动持续到退出"的状态无需手动 remove;若某类全局状态只在特定页面存在(如临时会话上下文),则在页面销毁时 remove 并置空引用,避免内存泄漏。此外,AppStorageV2 存的是可观察对象(@ObservedV2 类),与 V1 AppStorage 的"值拷贝存储"不同,连接方拿到的是同一引用,修改 @Trace 属性即为修改全局状态,这也要求写入方遵守单向数据流约定,避免多处同时改写导致状态发散。

六、跨组件通信的最佳实践

  1. 全局唯一且跨模块共享 → AppStorageV2.connect:如 WindowInfo。存储键用模块级常量(WINDOW_INFO_STORAGE_KEY),避免魔法字符串。
  2. 页面内跨层共享 → @Provider/@Consumer:key 统一字符串命名,Provider 尽量集中在页面根部(Index.ets),便于追溯。
  3. 父子直连 → @Param/@Event:能用 @Param 传参就不用 Provider,缩小影响面。
  4. AppStorageV2 存放可观察类对象,而非散落的基本类型:类对象配合 @Trace 支持属性级刷新,散落键值则难以组织与清理。
  5. AppStorageV2 与持久化解耦:AppStorageV2 只存内存态(进程内),如需跨启动恢复(断点、播放位置)应配合 Preferences 落地(见文章 25)。

七、总结与最佳实践

  1. V2 工程优先使用 AppStorageV2:connect 连接 @ObservedV2 类,类型安全、属性级可观察、全模块共享。
  2. WindowInfo 是"全局状态单例"的教科书实现:单例维护(WindowUtil)+ 全局存储(AppStorageV2)+ 组件观察(@Local + getWindowInfoState)三层协作。
  3. 页面级共享状态(主题、侧栏、路由)用 @Provider/@Consumer 树收敛在根部,避免滥用 AppStorageV2。
  4. 新旧代码共存时,V1 的 AppStorage 仅作兼容层,不新增依赖。
  5. 明确状态生命周期:应用级状态进 AppStorageV2,页面级状态进 Provider 树,临时状态用 @Local,职责清晰才能保证多设备工程的可维护性。
Logo

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

更多推荐