Kotlin Multiplatform for OpenHarmony 实战:为 MVIKotlin 实现单向数据流适配

大家好,我是熊猫钓鱼!欢迎大家和我一起探讨技术。希望您能点赞关注,谢谢!
摘要
本文是「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,纯逻辑零平台依赖,模拟器即可演示、无需任何权限或硬件。
目录
- 一、为什么要适配 MVIKotlin
- 单向数据流模型(Intent → Reducer → State → UI,Middleware / Label 旁路)
- 补齐状态管理拼图:与 Decompose、Essenty 的分工
- 二、路线取舍:等价复刻,且只有两层
- 系列两条路线回顾(真接口真实现 / 等价复刻)
- 为何 MVIKotlin 连引擎层都不需要(零系统 API、纯逻辑)
- 为何不复用上游 Kotlin 源码
- 三、语义层:把 MVI 契约画出来(不碰任何 @kit.*)
StateObservable<State>:可观察状态容器(订阅即回放)EventObservable<Event>:一次性事件流(不重放)Reducer<State, Intent>:纯函数Middleware<State, Intent, Label>:副作用三件套(bootstrapper / actor / postProcessor)Store/SimpleStore:串联闭环(accept 唯一入口、dispose 语义)
- 四、验收页:一个计数器讲清整条闭环
- Intent 唯一入口、Reducer 纯函数
- Middleware 三件套齐活(进页加载 / 异步回灌 / 阈值 Label)
- Label 不污染 State
- 运行效果描述
- 五、ArkTS 适配踩的坑
- 泛型 + 函数类型数组的可变性要求
- 密封类用联合类型近似
- 可选中间件成员判空
- 单线程与异步回灌
- dispose 语义与页面生命周期
- 六、和系列其它适配的对比
- 六库路线 / 层级 / 平台依赖 / 模拟器可演示对比表
- 七、版本与运行环境
- 适配平台、工具链、IDE、编译验证结果
- 八、小结与社区
- 三层架构 + 等价复刻在纯逻辑库上的高效性
- 社区引导语与 AtomCode 专属邀请链接、原创声明
本文是「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次

点击重新加载:

完成计数重置:

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



所有评论(0)