22 — 状态管理 V2 装饰器体系

一、引言

状态管理 V1 存在对象深度观察受限、@Prop 只同步一层、装饰器职责边界模糊等问题。HarmonyOS 5.0 起推出的状态管理 V2 以更清晰的职责划分、更强的类型约束和更细的观察粒度重塑了这套体系。本项目 multi-short-video 的 UI 层(视频页、播放器、评论、个人作品、首页)与全局状态(WindowInfo)全部采用 V2,是 V2 工程的完整样板。本文以 features/multishortvideoadaptivevideo/src/main/ets/view/AdaptiveVideo.etsview/AdaptiveAVPlayer.etscommon/multishortvideobase/src/main/ets/utils/WindowUtil.ets 为蓝本,拆解 V2 装饰器家族。

在这里插入图片描述

二、组件级 V2 装饰器:@ComponentV2、@Local、@Param、@Event

V2 自定义组件以 @ComponentV2 声明,内部状态分为三大类:@Local(组件内私有状态)、@Param(父组件传入的入参)、@Event(父组件注入的事件回调)。视频页 AdaptiveVideo.ets 的声明如下(真实代码):

@ComponentV2
export struct AdaptiveVideo {
  @Local windowInfo: WindowInfo = getWindowInfoState();
  @Local curIndex: number = 0;
  @Local showComment: boolean = false;
  @Local currentState: media.AVPlayerState = 'idle';
  @Local isSeeking: boolean = false;
  @Local seekToTime: number = -1;
  ...
}

播放器子组件 AdaptiveAVPlayer.ets 展示 @Param 与 @Event 的用法:

@ComponentV2
export struct AdaptiveAVPlayer {
  @Param currentIndex: number = -1;
  @Param index: number = 0;
  @Param currentSource: string = '';
  @Param seekToTime: number = -1;
  @Local currentState: media.AVPlayerState = 'idle';
  @Event onStateNotify: (state: media.AVPlayerState) => void = () => {};
  ...
}

设计要点:

  • @Local 只能就地初始化,禁止由外部赋值,天然杜绝"状态来源不明"。
  • @Param 由父组件传入、子组件只读,修改 @Param 不会回传父组件;父组件重新渲染时以最新值覆盖。
  • @Event 注入回调,子组件通过 this.onStateNotify(state) 把播放器状态"上抛"给父组件,形成单向数据流:状态在父、行为在父、子组件只上报事件。
  • 对比 V1:@Param 同时替代了 @Prop 的"单向入参"与 @Link 的部分场景,且支持按引用(@Param(‘xxx’, { type: … }) 深度同步)与按值两种传递策略。

三、@ObservedV2 与 @Trace:数据模型的深度观察

V2 用 @ObservedV2 装饰 class、@Trace 标记需要观察的成员属性,实现属性级精细观察。common/multishortvideobase/src/main/ets/utils/WindowUtil.ets 中的全局窗口信息对象即是典型:

@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;
  @Trace public statusBarTopVp: number = 0;
  ...
}

只有被 @Trace 标记的属性才具备可观察性,未标记属性修改不会触发 UI 刷新。这种"显式可观察"的设计避免了 V1 @Observed 整体代理带来的性能开销与误刷新。数据源模型 model/AvDataSourceModel.ets 当前为纯数据类(未加 @Trace),配合 @Local avDataSource: AvDataSourceModel[] 使用:当整个数组被替换时刷新列表,属性级刷新按需引入 @Trace 即可,体现了"观察粒度可按需选择"的灵活性。

四、@Provider/@Consumer:V2 的跨层级共享

V2 中 @Provider 提供数据、@Consumer 消费同名数据,替代 V1 的 @Provide/@Consume。根组件 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;
  @Provider('subTabIndex') subTabIndex: number = 4;
  ...
}

视频页与播放器跨层消费(AdaptiveVideo.ets 与 AdaptiveAVPlayer.ets 真实代码):

// AdaptiveVideo.ets
@Consumer('showSideComment') showSideComment: boolean = false;
@Consumer('showSideIndividual') showSideIndividual: boolean = false;
@Consumer('pathStack') pathStack: NavPathStack = new NavPathStack();
@Provider('windowSizeWidth') windowSizeWidth: Length = 0;
@Provider('windowSizeHeight') windowSizeHeight: Length = 0;
@Provider('commentCount') commentCount: Length = 0;

// AdaptiveAVPlayer.ets
@Consumer('currentTime') currentProgress: number = 0;
@Consumer('duration') duration: number = 0;

同文件内 @Provider 与 @Consumer 并存形成"转发":AdaptiveVideo 从根组件消费 showSideComment、pathStack,又把播放进度 currentTime/duration 提供给播放器与进度条。V2 还支持 @Provider 必须初始化、@Consumer 就地初始化不依赖外部顺序等约束,规避了 V1 中 @Consume 找不到提供方时的运行期告警。

五、V1 与 V2 差异对比与本项目选型

维度V1V2
组件装饰@Component@ComponentV2
私有状态@State@Local
入参@Prop/@Link@Param(支持按值/按引用)
事件回调普通函数属性@Event
类观察@Observed(整体代理)@ObservedV2 + @Trace(属性级)
跨层共享@Provide/@Consume@Provider/@Consumer
计算/监听@Watch(无前后值)@Monitor(带变化前后值)/ @Computed
编译期约束弱,多依赖运行期强,装饰器本地化/限制初始化

本项目选择 V2 的原因可归结为三点:一是播放器与窗口状态变化高频(timeUpdate 每 500ms 触发、windowSizeChange 频繁),属性级 @Trace + @Monitor 能将刷新与副作用收敛到精确节点;二是多设备工程组件复用度高,@Param/@Event 的单向数据流让组件契约清晰、易于跨 HAP 复用;三是 @Monitor 能拿到变化前后值,天然契合"拖动进度条 → 跳转播放 → 回写进度"这类联动逻辑(见文章 23)。

六、WindowInfo 全局对象的 V2 实现

WindowInfo 是全局唯一的窗口状态对象,由 getWindowInfoState() 通过 AppStorageV2.connect 连接后全局共享:

// common/multishortvideobase/src/main/ets/utils/WindowUtil.ets
export const WINDOW_INFO_STORAGE_KEY = 'MultiShortVideoWindowInfo';

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

WindowUtil 单例在窗口回调(onWindowSizeChange、onAvoidAreaChange)中直接修改 mainWindowInfo 的 @Trace 属性,任何组件内的 @Local windowInfo: WindowInfo = getWindowInfoState() 都能感知变化并刷新。于是"断点、避让区、沉浸状态"这些应用级信息,以类对象形式在 common 层定义、在任意产品/特性模块消费,成为驱动全项目自适应布局的状态中枢。

七、总结与最佳实践

  1. 组件内私有状态一律 @Local;只读入参用 @Param;行为上抛用 @Event,保持单向数据流。
  2. 数据模型按需加 @ObservedV2/@Trace:对象属性需要细粒度刷新时标记,纯数据容器不滥用。
  3. 跨层级共享用 @Provider/@Consumer,优先在页面根部(Index.ets)收敛全局 Provider,key 命名统一字符串常量。
  4. 高频状态(播放进度、窗口尺寸)借助 @Trace 属性级观察与 @Monitor 联动,避免整树刷新。
  5. 全局单例对象(WindowInfo)用 AppStorageV2.connect 连接,兼顾"全局唯一"与"可观察",具体见文章 24。
Logo

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

更多推荐