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](#一、为什么要适配 MVIKotlin)
    • 单向数据流模型(Intent → Reducer → State → UI,Middleware / Label 旁路)
    • 补齐状态管理拼图:与 Decompose、Essenty 的分工
  • 二、路线取舍:等价复刻,且只有两层
    • 系列两条路线回顾(真接口真实现 / 等价复刻)
    • 为何 MVIKotlin 连引擎层都不需要(零系统 API、纯逻辑)
    • 为何不复用上游 Kotlin 源码
  • [三、语义层:把 MVI 契约画出来(不碰任何 @kit.*)](#三、语义层:把 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 适配踩的坑](#五、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。

typescript 复制代码
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

相关推荐
Alice-YUE1 小时前
前端 × AI:从 Cursor 到 Transformer,一份完整认知路径
前端·人工智能·transformer·ai编程·前端开发·cursor
传奇开心果编程1 小时前
【ArkUI进阶练中学】第12课:应用架构演进与遗留系统迁移
学习·ui·华为·harmonyos
FanetheDivine2 小时前
学习Agent开发 10.延迟工具 deferred tools
agent·ai编程
第十人i2 小时前
Pi 1.0 正式发布:原生 MCP + Pi Durable,终端编程代理的重大升级
ai编程·pi
交付一线的ZLycan3 小时前
我用AI coding给旧手表(华米Amazfit GTR4)写了个番茄代办App
ai编程
传奇开心果编程3 小时前
【ArkUI进阶练中学】第13课:元服务与卡片开发
学习·ui·华为·harmonyos
晚安code3 小时前
Gemini 4 Argon 价格与用途:和 GPT-6、Claude 怎么选
ai编程
StrangeXin3 小时前
做AI项目,为什么我提议成立一个委员会
ai编程