在这里插入图片描述

大家好,我是熊猫钓鱼!欢迎大家和我一起探讨技术。希望您能点赞关注,谢谢!


摘要

本文是「OpenHarmony 鸿蒙化三方库适配」系列的第 7 篇,也是 arkivanov「状态管理三件套」的收官之作——Decompose 管组件化导航、Essenty 管生命周期与状态保持,MVIKotlin 管单向数据流(MVI)。MVIKotlin 用一条不可逆的单向数据流把交互框得明明白白:状态只能从 Intent 经过 Reducer 产生,副作用交给 Middleware(Bootstrapper / Actor / PostProcessor),一次性事件走 Label,没有第二条改状态的暗道。

适配上,MVIKotlin 属于「等价复刻」路线,且比 Decompose/Essenty 更轻:没有任何要桥接的系统 API,因此连引擎层都不需要,只有「语义层 + 验收页」两层。我们用纯 ArkTS 在 Mvi.ets 中还原了 StateObservable / EventObservable / Reducer / Middleware / Store / SimpleStore 六个契约(不引入任何 @kit.*),并在 MviDemo.ets 用计数器点亮整条闭环(Intent 唯一入口、Reducer 纯函数、Middleware 三件套、Label 不污染 State)。全文还总结了 ArkTS 适配踩的 5 个坑(泛型+函数类型数组、密封类近似、可选中间件、单线程异步回灌、dispose 语义),并与系列其它 5 个适配做了横向对比。

本适配基于 HarmonyOS SDK 6.0.0(20) + KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)开发,assembleHap 编译 BUILD SUCCESSFUL、零 ArkTS error,纯逻辑零平台依赖,模拟器即可演示、无需任何权限或硬件。


目录

本文是「OpenHarmony 鸿蒙化三方库适配」系列的第 7 篇,也是「状态管理三件套」的收官——Decompose 管组件化导航、Essenty 管生命周期与状态保持,MVIKotlin 管单向数据流。三者同出 arkivanov,在鸿蒙上全部用 ArkTS 等价复刻,正好补齐 CMP 应用架构的核心骨架。

一、为什么要适配 MVIKotlin

写 KMP/CMP 应用,绕不开状态管理。前面两篇我们把 Decompose(组件化导航)和 Essenty(生命周期/状态保持)搬上了鸿蒙,但「用户点了按钮之后状态怎么变、副作用怎么处理」这件事,一直缺一块拼图。MVIKotlin 恰好补上——它用一条不可逆的单向数据流把交互框得明明白白:

        ┌─────────── accept(intent) ───────────┐
        ▼                                       │
   Intent ──▶ Reducer(state, intent) ──▶ State ──▶ (UI)
                 │                             ▲
                 │ Middleware                  │
                 ▼                             │
        bootstrapper / actor / postProcessor ──┘
                 │                             │
                 └──── Label(一次性事件)──────┘

这套模型的好处是「状态只能从 Intent 经过 Reducer 产生」,没有第二条改状态的暗道,调试和测试都简单。
在这里插入图片描述

鸿蒙的 ArkUI 本身不提供这种架构(那是应用层的事),所以我们的适配目标很纯粹:把 MVI 契约原样还原成 ArkTS,让上层业务零成本迁移。

使用Dev Eco最新版开发代码如下:请添加图片描述

二、路线取舍:等价复刻,且只有两层

我们这套系列的适配有两条路线:

  • 路线 A:真接口真实现——Ktor(网络栈)、Notifier(通知)、kable(BLE),系统有对应能力,引擎层去桥 @kit.*;
  • 路线:等价复刻——Decompose、Essenty,系统没有对应物,纯 ArkTS 把契约画出来。

MVIKotlin 属于后者,而且比前两者更"轻":它没有任何要桥接的系统 API,因此连引擎层都不需要,只有「语义层 + 验收页」两层。
在这里插入图片描述

这也是它在本批候选库里最容易落地的原因——纯逻辑、零平台依赖、模拟器直接跑、还能丢进 Node 离线测。

为什么不复用上游 Kotlin 源码?上游是 Kotlin,要在鸿蒙跑要么等 ohosArm64 目标、要么搬 Kotlin/Native 运行时,成本不可控;而 MVI 契约本身就是几十行纯逻辑,复刻比编译上游划算得多。

三、语义层:把 MVI 契约画出来(不碰任何 @kit.*)

核心文件 Mvi.ets 定义了六个角色,一一对应 MVIKotlin:

StateObservable<State> —— 可观察状态容器

持有当前 State,订阅即回放当前值(避免首帧空白),写新值就向所有订阅者派发。对应 MVIKotlin 的 StateObservable。

export class StateObservable<State> {
  private current: State;
  private readonly listeners: Array<(value: State) => void> = [];
  constructor(initial: State) { this.current = initial; }
  get(): State { return this.current; }
  set(value: State): void {
    this.current = value;
    for (const listener of this.listeners) { listener(value); }
  }
  subscribe(listener: (value: State) => void): () => void {
    this.listeners.push(listener);
    listener(this.current);            // 订阅即回放
    return (): void => { /* 取消订阅 */ };
  }
}

EventObservable<Event> —— 一次性事件流

用于 Label 和 Intent 回流。关键区别:事件不被重放,订阅只收得到之后发出的——这正是「一次性事件」应有的语义(你不想在屏幕旋转后重弹一次 Toast)。

Reducer<State, Intent> —— 纯函数

invoke(state, intent): State,唯一允许算新 State 的地方,没有副作用。

Middleware<State, Intent, Label> —— 副作用三件套

把 MVIKotlin 的 Bootstrapper / Actor / PostProcessor 合成一个接口,三个成员全是可选,只放你需要的:

  • bootstrapper:启动期副作用,进页即发 Intent / Label(如加载初始数据);
  • actor:处理某个 Intent 的异步/副作用,结果回灌成新 Intent / Label(如网络请求完成后派发);
  • postProcessor:状态变更后派生一次性 Label(如到达阈值弹提示),没有就返 null。

Store / SimpleStore —— 把上面串成闭环

accept(intent) 是唯一改状态的入口:先走 Reducer 算新 State 落盘,再跑 postProcessor 发 Label,最后跑 actor 处理副作用。整个顺序和 MVIKotlin 一致。dispose() 之后的 Store 不再响应 accept,和上游 dispose 语义对齐。

四、验收页:一个计数器讲清整条闭环

在这里插入图片描述

MviDemo.ets 用计数器把四个角色都点亮:

  • Intent 是唯一入口:+1 / -1 / 重置 / 重新加载 全部走 store.accept(intent),没有任何地方直接改 @State;
  • Reducer 是纯函数:inc/dec/reset/load/loaded 都是 State → State 的确定性变换;
  • Middleware 三件套齐活:
    • bootstrapper 进页即 dispatch('load');
    • actor 在收到 load 时用 setTimeout(600ms) 模拟异步加载,再 dispatch('loaded') 回灌——这正好演示了「副作用后状态回流」;
    • postProcessor 在 count === 10 时返回 'celebrate' Label;
  • Label 不污染 State:到 10 的提示走 labels 流,不会混进可观察状态里被反复重放。

跑起来你会看到:进页先「加载中…」约 600ms 变回计数,点 +1 点到 10 顶部弹出「🎉 计数到达 10!」,事件日志把每次 Intent / Label 都记下来——单向数据流闭环一目了然。

五、运行与问题

部署:
在这里插入图片描述

运行如下:
在这里插入图片描述
我们点击计数:累计10次
在这里插入图片描述
点击重新加载:
在这里插入图片描述
完成计数重置:
在这里插入图片描述

  1. 泛型 + 函数类型数组:StateObservable 内部用 Array<(value: State) => void> 存订阅者。ArkTS 支持泛型类与函数类型数组,但字段必须带 readonly 且初始化,否则编译报状态可变性错误。
  2. 密封类用联合类型近似:MVIKotlin 的 Intent/Label 通常是 sealed class,ArkTS 没有 sealed,改用 'inc' | 'dec' | ... 字符串字面量联合,switch 配 default 兜底,既保留穷尽检查又编译通过。
  3. 可选中间件成员:Middleware 三个方法都标 ?,对象字面量只给用到的那几个即可;调用前用 !== undefined 判空,规避 ArkTS 对可选成员调用的红线。
  4. 单线程与异步回灌:鸿蒙 ArkTS 是单线程,actor 里用 setTimeout 模拟"异步副作用后回灌 Intent"——真实场景换成网络/数据库回调同样套这个模式。
  5. dispose 语义:aboutToDisappear 里 store.dispose(),避免页面销毁后回调还往已销毁的 @State 写;accept 开头判 disposed 直接 return。

六、和系列其它适配的对比

库路线层级平台依赖模拟器可演示
Decompose等价复刻语义+验收无✅(逻辑可离线测)
Essenty等价复刻语义+验收无✅
Ktor真接口真实现语义+引擎+验收@ohos.net.http✅(需网络权限)
Notifier真接口真实现语义+引擎+验收@kit.NotificationKit✅(需通知授权)
kable真接口真实现语义+引擎+验收@kit.ConnectivityKit⚠️(BLE 需真机,模拟器用模拟演示)
MVIKotlin等价复刻语义+验收无✅(零权限零硬件)

可以看到:纯逻辑类(Decompose/Essenty/MVIKotlin)是最"好实现"的一档,而 MVIKotlin 因为连引擎层都不需要,是其中落地成本最低、演示最顺的一个。

七、版本与运行环境

  • 适配目标平台:HarmonyOS SDK 6.0.0(20)(API 20)
  • 工具链:KMP&CMP 鸿蒙社区工具链 v1.1.0(Kotlin 2.2.21 / CMP 1.9.2)
  • IDE:DevEco Studio 26.0.0 Release
  • 编译验证:assembleHap BUILD SUCCESSFUL,零 ArkTS error

八、小结

终于写完了,我觉得MVIKotlin 的适配再次验证了「三层架构 + 等价复刻」在纯逻辑类 KMP 库上的高效:

几十行 ArkTS 把单向数据流契约还原,零平台依赖、模拟器即跑、可离线单测。

配合前面 Decompose/Essenty,arkivanov 状态管理三件套在 OpenHarmony 上正式集齐。希望大家也能有所收获!

欢迎加入 KMP&CMP 鸿蒙社区:https://atomgit.com/CPF-KMP-CMP
AtomCode 专属邀请链接:
https://developer.huaweicloud.com/codeartsco.html?source=dmzntgwatomgit1&sourcead=dmzntgwatomgiths

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐