开源鸿蒙平台 KMP 三方库「moko-mvvm」适配全流程
本文记录
moko-mvvm接入 OpenHarmony 的完整过程,覆盖 MVVM 架构库盘点、鸿蒙 kotlinx 制品线选型、真实引擎编译、Kotlin 2 expect/actual 兼容修复、ohosArm64/ohosX64目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
moko-mvvm是 icerockdev 出品、Kotlin Multiplatform 生态使用最广的 MVVM 架构库:ViewModel提供viewModelScope生命周期与onCleared取消语义,EventsDispatcher以弱引用持有监听者并在其未附加时缓冲事件,LiveData/MediatorLiveData构成响应式状态层(map/flatMap/mergeWith/distinct/mediator/all变换链,asFlow/asLiveData与协程互转),CFlow/CStateFlow提供带DisposableHandle取消语义的订阅包装,ResourceState负责加载态建模。本次适配沿用社区已审核路线(cmp_qrose → KStore → SimpleMVI → orbit-mvi)并有一处新挑战:上游停留在 2022 年工具链(Kotlin 1.8.10 / Gradle 7.6.1 / coroutines 1.6.4),跨代升级到鸿蒙线后 Kotlin 2(K2)编译器收紧了 expect/actual 检查——真实的 MVVM 引擎(六个库模块的上游源码)被原样编译成 OpenHarmony KLIB,仅 4 个声明文件按 K2 规则补显式标注(签名与上游实现逐字相同,零行为变化),一个带协程生命周期、事件缓冲、mediator 重算的完整 MVVM 引擎运行在真机上,ArkUI 页面作为验收宿主。
项目地址: AtomGit/oh-tpc/moko-mvvm.git
上游仓库: icerockdev/moko-mvvm
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
moko-mvvm 为 Kotlin Multiplatform 应用提供"视图模型 ↔ 状态 ↔ 事件"这一层的完整答案:ViewModel() 构造即得随生命周期取消的 viewModelScope,EventsDispatcher<Events> 一行挂接导航 / 提示事件,MutableLiveData / MediatorLiveData 把输入校验、派生状态、数据合并都收敛为可观察链,ResourceState<T, E> 统一 Loading / Success / Failed / Empty 建模,CFlow 系列把 kotlinx Flow 包装成可跨语言订阅的句柄。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要这套 MVVM 能力,但上游构建(Kotlin 1.8.10 / Gradle 7.6.1 / AGP 7.4.2)不但不会生成 OpenHarmony 变体,连承载鸿蒙工具链的 Gradle 版本都不够。
如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明 MVVM 引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证 ViewModel 生命周期、EventsDispatcher 事件语义、LiveData 响应式层、协程调度、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。 |
| 工具链跨代 | 上游 Kotlin 1.8.10 / Gradle 7.6.1(因 KT-55751 钉死)/ AGP 7.4.2,与鸿蒙工具链(Kotlin 2.4.20 线 / Gradle 9.x)隔着两个大版本。 |
| 依赖矩阵 | 引擎依赖 kotlinx-coroutines-core 1.6.4(2022 native-mt 时代),社区 ohos 制品线上没有它的变体,也不存在同体重建;测试侧还有 moko-test / kswift-runtime / kotlin-test-junit 各自的变体问题。 |
| K2 收紧 | Kotlin 2 编译器对 expect/actual 类新增模态一致性检查,上游 CStateFlow / CMutableStateFlow 的旧写法直接编译失败。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 协程语义跨不了桥 | viewModelScope 默认挂 Dispatchers.Main,而鸿蒙线 Main 跨线程派发有已知问题;CFlow.subscribe() 无参重载也写死 Main。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:六个引擎模块的 ohos 编译、根构建替换(cmp_qrose 模式)、K2 兼容性最小修复和 C ABI/N-API 桥接层。MVVM 语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示 trace。
1.2 库提供的能力
Demo 提供一个真实的登录 ViewModel 会话与五个引擎演示,对应桥接的全部入口:
- 创建(create):构造
LoginViewModel,三个 LiveData 依次addObserver(上游契约:附加即回放当前值),EventsDispatcher挂接弱引用监听者,viewModelScope经TestViewModelScope注入事件循环; - 输入(input):写
username/password两个MutableLiveData,mediator 校验链(map× 2 +all)即时重算; - 登录(login):
viewModelScope.launch里跑真实协程(delay / 恢复 / 双分支),输出ResourceState.Loading → Success/Failed状态流与routeToMainScreen/showToast事件; - 销毁(clear):
onCleared()取消viewModelScope,随后再登录证明协程未运行——post-clear login 零状态变化; - 引擎五连(eventsDispatcher / livedata / debounce / flow / state):事件缓冲与回放、observer 生命周期与变换链、防抖合并、订阅取消语义、
data()/error()派生链; - 版本握手:
mokoMvvmVersion()返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| 字段 | 作用 |
|---|---|
ok | 操作是否成功,失败时 error 带原文。 |
op / sessionId | 回显本次操作与会话 id。 |
snapshot / before / after | 会话状态快照(isLoading / state / valid / scopeActive / cleared 五元组)。 |
trace | 引擎侧事件日志(observer 回调、mediator 重算、事件分发、生命周期跳变),逐行回传。 |
delivered / collected | eventsDispatcher 送达时序 / debounce 与 flow 的收集结果。 |
引擎侧完整能力(flatMap / mergeWith / mapBuffered / MediatorLiveData.addSource / asFlow / asLiveData / CFlow 冷流订阅等)全部在上游 commonMain / nonAndroidMain 里原样随编译进入 .so,JVM 侧保留 10 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的 MVVM 逻辑没有漂移。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 六个模块的全部上游源文件原样编译,不提取、不重写;仅 4 个 mvvm-flow 声明文件按 K2 规则补显式标注(签名=上游实现,零行为变化),新增 1 个 12 行 ohos actual(全仓唯一新增库源文件)。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);compose 适配模块、android 专属模块、moko-resources 包装、XCFramework 工程、上游 sample、mvvm-build-logic 全部留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | coroutines 采取双线:ohos 线取社区同体重建 1.11.0-0.1.0-07,jvm 线取 Maven Central 原版体 1.11.0——换线且主体升到 ohos 线的基准版(上游 1.6.4 无 ohos 制品),上游测试零改动全绿。 |
| UI 完整 | 两页签覆盖全部桥接入口:登录会话四步流 + 引擎五连演示 + 版本握手。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。
moko-mvvm的 compose 适配模块(上游钉 JetBrains Compose 1.3.1)按 multiplatform-settings 审核先例留盘为 P2;MVVM 引擎本身不碰 UI,适配面天然落在核心六模块。
二、实现路线图
第 1 阶段:项目初始化 ── 模块与依赖盘点、查重、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、
Gradle 7.6.1→9.6.1、coroutines 双线、K2 expect/actual 修复
第 3 阶段:桥协议建模 ── 扁平 JSON 契约、LoginViewModel 会话模型、错误通道
第 4 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主 ── 版本握手、登录表单、引擎五连、trace 日志区
第 6 阶段:示例与验证 ── 双 ABI .so、JVM 测试全绿、C++ 冒烟编译、HAP、签名、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
mvvm-internal/ 上游内部件(进 ohos 构建,jvm + ohosArm64 + ohosX64)
mvvm-internal/src/ohosMain/ 新增:runOnMainThread actual(12 行,全仓唯一新增库源文件)
mvvm-core/ 上游 ViewModel + EventsDispatcher expect/actual(进 ohos 构建)
mvvm-flow/ 上游 CFlow 包装(进 ohos 构建;4 个声明文件按 K2 补标注)
mvvm-livedata/ 上游 LiveData 响应式层(进 ohos 构建,零源码改动)
mvvm-state/ 上游 ResourceState(进 ohos 构建,100% commonMain)
mvvm-test/ 上游测试工具箱(进 ohos 构建,桥与 jvm 测试共用)
example/nativeApp/ Kotlin/Native 动态库(Bridge.kt + Demo 域 + MiniJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
mvvm-compose / mvvm-flow-compose / mvvm-livedata-compose
compose 适配(上游钉 Compose 1.3.1),留盘不进 settings
mvvm-flow-resources / mvvm-livedata-resources
moko-resources 包装(android+ios 专属),留盘不进 settings
mvvm-flow-material / mvvm-livedata-material / mvvm-livedata-glide /
mvvm-livedata-swiperefresh / mvvm-databinding / mvvm-viewbinding
纯 Android 模块,留盘不进 settings
mvvm-flow/apple/ XCFramework + SwiftUI 发布工程,留盘
sample/ sample-declarative-ui/ mvvm-build-logic/ 上游样例与构建逻辑,留盘
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
*.upstream 上游构建文件备份(审核对照用)
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include 六个引擎模块与 :example:nativeApp,rootProject.name = "ohos_moko_mvvm"。上游的 dokka / nexus-publish / mvvm-build-logic classpath / 全局 coroutines force 全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven) |
| JDK | 21(org.gradle.java.home 指向 Eclipse Adoptium) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm 保留作交叉验证) |
| Gradle | 9.6.1(上游 wrapper 7.6.1 升级;上游因 KT-55751 钉 7.6.1,该限制只属于 Kotlin 1.8 线) |
| kotlinx | coroutines ohos 线 1.11.0-0.1.0-07 / jvm 线 1.11.0(双线,见 2.1) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 5.1.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.mokomvvm |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
| 模块 | commonMain 依赖 | 平台源集 | 处置 |
|---|---|---|---|
mvvm-internal | coroutines | nonAndroid / native / apple / js / jvm / linux / mingw | 整体编译(jvm + 双 ohos) |
mvvm-core | mvvm-internal + coroutines(androidMain 另有 androidx.lifecycle) | common expect + android / nonAndroid actual | 整体编译(androidMain 留盘不编) |
mvvm-flow | mvvm-core + kswift-runtime | common expect + android / ios / nonAndroid actual | 整体编译(kswift 依赖随 iosMain 一起剥离;4 文件 K2 标注) |
mvvm-livedata | mvvm-core + kswift-runtime(androidMain 另有 androidx livedata) | common + nonAndroid + android + ios + mobileMain | 整体编译(mobileMain / androidMain / iosMain 留盘) |
mvvm-state | mvvm-livedata | 100% commonMain | 整体编译(零平台源码) |
mvvm-test | mvvm-internal + mvvm-core + kotlin-test | common + android / nonAndroid | 整体编译(桥要用 TestViewModelScope) |
expect/actual 全库盘点(ohos 侧的解析路径):
| expect(位置) | ohos 落到哪个 actual | 需要新写吗 |
|---|---|---|
ViewModel(core commonMain) | nonAndroidMain actual(scope + onCleared 取消) | 否,重接线后自动继承 |
EventsDispatcher(core commonMain) | nonAndroidMain actual(弱引用 + 缓冲 + runOnMainThread) | 否 |
runOnMainThread(internal nonAndroidMain) | linux/mingw 是直接执行、apple 是 GCD | 是——新增 ohosMain actual(12 行) |
WeakReference(internal nonAndroidMain) | nativeMain actual(kotlin.native.ref.WeakReference typealias) | 否(需模块级 optIn,见 2.4) |
CFlow / CStateFlow / CMutableStateFlow(flow commonMain) | nonAndroidMain actual | 否(但 4 文件需 K2 标注,见 2.4) |
LiveData / MutableLiveData / MediatorLiveData(livedata commonMain) | common 声明 + nonAndroid actual | 否 |
postValue(livedata mobileMain) | android / ios 专属 | 不进构建(已知限制) |
createTestEventsDispatcher(test commonMain) | nonAndroidMain actual | 否 |
mvvm-state 值得单独说明:它的全部实现(ResourceState / StateExt / StateLiveData*)都站在纯 Kotlin 与自家 livedata 之上,没有任何平台源集——所以这个模块在鸿蒙侧一行代码都不用补。真正的新增面只有 mvvm-internal 的 runOnMainThread,这就是整个 fork 唯一的新增库源文件:
// mvvm-internal/src/ohosMain/kotlin/dev/icerock/moko/mvvm/internal/runOnMainThread.kt
// Mirrors the upstream linuxX64Main / mingwX64Main actuals: block() runs
// inline on the calling thread. The ohos bridge is called from the ArkTS
// (main) thread only, so "run on main thread" degenerates to direct
// execution on that thread, and EventsDispatcher listeners fire
// synchronously during dispatchEvent.
package dev.icerock.moko.mvvm.internal
actual fun runOnMainThread(block: () -> Unit) = block()
1.4 页面边界
标题区 版本握手行(moko-mvvm 0.16.1-ohos.1 · kotlin 2.4.20-0.1.0-07 · coroutines 1.11.0-0.1.0-07)
会话行 session id + 引擎快照(isLoading / state / valid / scopeActive / cleared)
Tab 切换 ViewModel / Engine
ViewModel 页签 username / password 输入框(密码掩码)
四步按钮:创建会话(深蓝) / 填入有效凭据 / 填入无效凭据 / 登录(深蓝) / 销毁(暗红)
Engine 页签 五个演示按钮:EventsDispatcher / LiveData / Debounce / CFlow / ResourceState
日志区 engine trace(引擎逐行回传,`---` 分隔标题高亮,等宽字体,底部滚动)
页面启动时先调 mokoMvvmVersion() 握手,再经 op=version 取四元组详情(版本 / 上游 / Kotlin 线 / coroutines 线)写入标题行。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上 coroutines 的 ohos 制品。用 .module 元数据验证:
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
variants: ohosArm64ApiElements-published / ohosX64ApiElements-published / jvmApi-published
三个关键结论:其一,ohos 制品存在且同时发布 jvm 变体,jvm 交叉验证可以用同一个版本号;其二,ohos 线最早只有 1.10.2 / 1.11.0 系,上游钉的 1.6.4(native-mt 时代)没有 ohos 制品、也不存在同体重建——版本主体必须升到 ohos 线的基准 1.11.0;其三,与 KStore"坐标逐字相同只换仓库"不同,本仓是"主体升级 + 双线隔离",jvm 线取 Maven Central 的 1.11.0 原版体,ohos 线取 1.11.0-0.1.0-07——jvm 交叉验证跑的正是 ohos .so 里执行的那一版 coroutines 行为。
测试依赖侧:moko-test 0.6.1(上游 commonTest 用)Maven Central 有 jvm 变体,测试只跑 jvm,保留原坐标;turbine 问题在 moko-mvvm 不存在(上游没用),但 kotlin-test-junit 是 jvm/android 专属坐标,ohos 编译解析不到——mvvm-test 的构建脚本换成多平台坐标 kotlin("test")(jvm 侧自动落到 kotlin-test-junit,ohos 侧落到社区 kotlin-test klib),这是构建脚本级改动,不碰源码;kswift-runtime 只被不编译的 iosMain/androidMain 引用,直接删除。
2.2 工具链钉版(为什么是 2.4.20-0.1.0-07,Gradle 为什么升 9.6.1)
社区早期钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose 用),multiplatform-settings 因为不引 coroutines 也停留在这条线。但 coroutines 1.11.0-0.1.0-07 依赖 kotlin-stdlib 2.4.20-0.1.0-07(元数据 2.4.0),Kotlin 2.2.21 只能读到 2.3.0,编译期直接报:
Class 'kotlin.Unit' was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.4.0, expected version is 2.3.0 or less.
工具链版本跟着依赖走:Kotlin 钉 2.4.20-0.1.0-07(orbit-mvi / SimpleMVI / krogress 同款验证组合)。Gradle 侧与 KStore 不同:上游因 KT-55751 把 wrapper 钉死 7.6.1(注释原文 “can’t update to gradle 8 with kotlin 1.8”),该限制只属于 Kotlin 1.8 线;鸿蒙 Kotlin 2.4.20 线要求现代 Gradle,直接升到 9.6.1(orbit-mvi 实测组合)。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
// HarmonyOS Kotlin toolchain must resolve first, otherwise ohosArm64()
// fails with "Unresolved reference" at configuration time.
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
gradlePluginPortal()
google()
mavenCentral()
}
}
// dependencyResolutionManagement 同款置前 + repositoriesMode = PREFER_PROJECT
rootProject.name = "ohos_moko_mvvm"
include(":mvvm-internal")
include(":mvvm-core")
include(":mvvm-flow")
include(":mvvm-livedata")
include(":mvvm-state")
include(":mvvm-test")
include(":example:nativeApp")
2.3 编译真实引擎(源码集层级重接)
上游通过 mvvm-build-logic 的 kmp-library-convention 搭了三层自定义中间源集:commonMain → nonAndroidMain → nativeMain → appleMain,src/nonAndroidMain(ViewModel / EventsDispatcher / LiveData 的非 Android actual)与 src/nativeMain(WeakReference actual)是引擎的关键平台文件。新构建按 orbit-mvi 验证过的接法,每个模块重连三条边:
// mvvm-internal/build.gradle.kts(其余模块同构)
plugins { alias(libs.plugins.kotlin.multiplatform) }
kotlin {
applyDefaultHierarchyTemplate() // ohosArm64/ohosX64 归入默认 nativeMain 组
compilerOptions { optIn.add("kotlin.experimental.ExperimentalNativeApi") }
jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } } // 交叉验证
ohosArm64()
ohosX64()
sourceSets {
val nonAndroidMain by creating { dependsOn(commonMain.get()) }
nativeMain.get().dependsOn(nonAndroidMain) // ohos 继承 nonAndroidMain + nativeMain
jvmMain.get().dependsOn(nonAndroidMain)
ohosMain.dependencies { api(libs.kotlinx.coroutines.core) } // 1.11.0-0.1.0-07
jvmMain.dependencies { api(libs.kotlinx.coroutines.core.jvm) } // 1.11.0
}
}
三个要点:默认层级模板的 nativeMain 自动收录磁盘上的 src/nativeMain,ohos target 免费获得 WeakReference actual;src/appleMain / src/jsMain / src/linuxX64Main / src/mingwX64Main / src/androidMain / src/iosMain 对应 target 不在构建中,目录原样留盘不编译;coroutines 走双线声明——commonMain 坐标一个不改,ohosMain / jvmMain 分线(KMP 源集依赖是并集语义,commonMain 里的 Dispatchers / CoroutineScope / StateFlow 引用按 target 解析到对应制品线)。实测:
./gradlew compileKotlinOhosArm64 compileKotlinOhosX64 # 六个模块全部 BUILD SUCCESSFUL
引擎的全部上游源文件一个没动,只有那 12 行 ohos actual 是新的。
2.4 Kotlin 2(K2)expect/actual 修复(本适配特有难点)
鸿蒙线 Kotlin 2.4.20 携带 K2 编译器,对 expect/actual 类的检查比上游的 1.8 严格,产生两类问题:
(a)kotlin.native.ref.WeakReference 需要 opt-in。mvvm-internal 的 nativeMain actual typealias 指向的 kotlin.native.ref.WeakReference 在鸿蒙线标注了 @ExperimentalNativeApi。处置:受影响模块的 compilerOptions 加模块级 optIn(构建脚本级,源码零改动;optIn 经 mvvm-core 对 WeakReference 的引用传导,故全部库模块统一添加)。
(b)CStateFlow / CMutableStateFlow 的"模态不匹配"错误:
e: CStateFlow.kt:13:18 The 'expect' and the 'actual' declarations are incompatible.
expect: public abstract expect override val replayCache: List<T>
actual: public open override val replayCache: List<T>
reason: the modalities are different ('abstract' vs 'open')
原因:expect 类未声明这些成员(从 StateFlow 继承为抽象),nonAndroid actual 却给了 open 实现;K1 不检查、K2 要求一一对应。实测 -Xexpect-actual-classes 标志不能消除该错误;正确解法是社区迁移的标准做法——expect 侧显式声明同名抽象 override,actual 侧对应加 actual 标记:
// commonMain expect(声明=上游实现的签名,逐字相同)
expect open class CStateFlow<T>(flow: StateFlow<T>) : StateFlow<T> {
override val value: T
override val replayCache: List<T>
override suspend fun collect(collector: FlowCollector<T>): Nothing
}
// nonAndroidMain actual(原实现 + actual 标记)
actual override val replayCache: List<T> get() = flow.replayCache
本仓对 4 个文件做了该修复(CMutableStateFlow 同款六个成员),上游 master 至今仍是 Kotlin 1.8 旧写法(无处可移植),由本 fork 完成并在 README.OpenHarmony_CN.md §18 完整记录。其余 expect/actual 对(ViewModel / EventsDispatcher / LiveData 家族 / WeakReference / createTestEventsDispatcher)都是"expect 自声明成员 ↔ actual 带 actual 标记实现"的规范形态,K2 下原样通过,零改动。
2.5 测试依赖的处置:ohos 侧只编不测
ohos 线没有设备端测试运行器,且 moko-test 无 ohos 变体。处置是两条纪律的组合:测试统一在 JVM 跑,根构建全局禁用 ohos 测试任务:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
这样上游测试文件(moko-test 用例原样引用)零改动保留,ohos 侧只承担编译与链接。
第 3 阶段:桥协议建模
3.1 JSON 契约
dev.icerock.moko.mvvm/**(上游引擎六模块,原样编译)
↓ ViewModel / EventsDispatcher / LiveData / CFlow / ResourceState
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API mokoMvvmVersion() / mokoMvvmDispatch()
↓
ArkUI Index.ets(两页签)
请求与响应:
请求 : {"op":"create"}
{"op":"input","sessionId":1,"username":"admin","password":"12345678"}
{"op":"login","sessionId":1} {"op":"clear","sessionId":1}
{"op":"eventsDispatcher" | "livedata" | "debounce" | "flow" | "state"}
响应 : {"ok":true,"op":"...","sessionId":1,"snapshot":"isLoading=false;state=...;valid=true;...","trace":[...]}
{"ok":false,"op":"...","error":"..."}
两个设计决策:trace 是引擎侧事件日志数组(observer 回调、mediator 重算、事件分发逐行回传),ArkTS 原样渲染——MVVM 引擎的语义时序就这样第一次在真机页面上可见;snapshot 是五元组快照(isLoading / state / valid / scopeActive / cleared),login 响应同时带 before / after 两个快照,状态跳变一眼可读。
3.2 会话模型与 TestViewModelScope
桥内会话这样开:
runBlocking {
val driverJob = SupervisorJob()
val scope = CoroutineScope(coroutineContext + driverJob) // 继承 runBlocking 事件循环
TestViewModelScope.setupViewModelScope(scope) // ← 上游 mvvm-test 的官方机制
val vm = LoginViewModel() // 构造时读到注入的 scope(同线程)
vm.isLoading.addCloseableObserver { trace += "observer<isLoading>: $it" }
vm.loginState.addObserver { trace += "observer<loginState>: ${describeState(it)}" }
vm.eventsDispatcher.listener = object : LoginViewModel.EventsListener { /* 事件进 trace */ }
}
TestViewModelScope.setupViewModelScope 正是上游单元测试替换 viewModelScope 的 public API——登录协程的 delay / 恢复 / 取消全部在 ArkTS 调用线程的事件循环上确定性执行。login op 里 runBlocking { vm.login(); driverJob.children.joinAll() }(带 5 秒兜底超时)等待协程完成后回传 trace;clear op 调 onCleared() 后再 login,launch 落在已取消的 scope 上、协程体一行不跑——"销毁后零副作用"由引擎自身语义证明。
suspend 调用统一用 runBlocking 收进同步 C ABI:鸿蒙线 Dispatchers.Main 的跨线程派发有已知问题(resaca 仓适配时记录过),桥全程不碰 Main。
3.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("MokoMvvmDispatch")
fun mokoMvvmDispatch(request: String): String {
var op = ""
return try {
val json = MiniJson.parse(request)
op = json.str("op") ?: ""
when (op) { "version" -> ...; "create" -> ...; /* 十个 op */ }
} catch (t: Throwable) {
JsonBuilder().bool("ok", false).str("op", op)
.str("error", "${t::class.simpleName}: ${t.message}").toString()
}
}
非法 JSON、缺 op / sessionId、未知会话、不支持的 op,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进日志区。
第 4 阶段:原生桥接(技术难点)
4.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libmokomvvm_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosmokomvvm.so)
│ MokoMvvmDispatch / MokoMvvmVersion
▼
UTF-8 JSON + DisposeString
直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制 MVVM 语义,所以选择 C ABI + JSON。
4.2 Kotlin/Native 导出函数
@CName("MokoMvvmDispatch")
fun mokoMvvmDispatch(request: String): String // 十个 op 一口进
@CName("MokoMvvmVersion")
fun mokoMvvmVersion(): String = "0.16.1-ohos.1" // 版本握手
| C ABI 符号 | ArkTS 方法 | 覆盖内核 |
|---|---|---|
MokoMvvmDispatch | mokoMvvmDispatch(req) | 会话四步 + 引擎五连,共十个 op |
MokoMvvmVersion | mokoMvvmVersion() | fork 版本握手(真机版本行的数据来源) |
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"mokoMvvmDispatch", nullptr, NapiMokoMvvmDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"mokoMvvmVersion", nullptr, NapiMokoMvvmVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosmokomvvm_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。include/libohosmokomvvm_api.h 直接采用 Kotlin/Native 链接期生成的真实头文件(from build/bin/ohosArm64/debugShared),符号名与 napi_init.cpp 逐一核对。
4.4 CMake 和类型声明
add_library(ohosmokomvvm SHARED IMPORTED)
set_target_properties(ohosmokomvvm PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(mokomvvm_napi SHARED napi_init.cpp)
target_link_libraries(mokomvvm_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosmokomvvm)
OHOS_ARCH 为空或 libohosmokomvvm.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const mokoMvvmDispatch: (request: string) => string;
export const mokoMvvmVersion: () => string;
第 5 阶段:ArkUI 宿主
5.1 版本握手和触发操作
import { mokoMvvmDispatch, mokoMvvmVersion } from 'libmokomvvm_napi.so';
aboutToAppear(): void {
const v = this.dispatch('{"op":"version"}') as VersionResponse;
this.versionLine = 'moko-mvvm ' + v.version + ' (upstream ' + v.upstream + ') · kotlin ' +
v.kotlin + ' · coroutines ' + v.coroutines; // 四元组全部来自真实 Native 调用
}
private dispatch(request: string): BridgeResponse {
return JSON.parse(mokoMvvmDispatch(request)) as BridgeResponse; // 异常通道统一渲染
}
ViewModel 页签的四步流共用 dispatch:create 保存返回的 sessionId;input 带输入框当前值(两个快捷按钮注入有效 / 无效凭据);login 先 input 再 login 一气呵成;clear 后再点登录,trace 里 post-clear 段零新增——协程取消的可视化证明。
5.2 页面交互
ViewModel 页签自上而下:用户名输入框、密码输入框(掩码)、深蓝"创建会话"按钮、有效 / 无效凭据双按钮、深蓝"登录"按钮、暗红"销毁"按钮与提示文案;Engine 页签五个演示按钮(EventsDispatcher / LiveData / Debounce / CFlow / ResourceState)。底部 engine trace 日志区深色面板、等宽字体,--- 标题行高亮,逐行渲染引擎回传的 trace、快照与 delivered / collected 结果。
5.3 对比度和错误边界
主操作深蓝按钮白字,销毁暗红对齐破坏性语义;ok:false 时错误原文进日志区。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进日志;未创建会话就点登录 / 销毁时页面先给出提示、不发起 Native 调用。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosmokomvvm)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/example/nativeapp/
│ └── Bridge.kt @CName × 2 + Demo 域 + MiniJson(继承 cmp_qrose 审核模式)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.mokomvvm、versionName 0.16.1-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/{arm64-v8a,x86_64}/ linkDebugShared* 经 copyKnSo* 回填的三件套(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so、$ORIGIN rpath
├── src/main/cpp/napi_init.cpp N-API 导出、参数检查、DisposeString
├── src/main/cpp/include/libohosmokomvvm_api.h
├── src/main/cpp/types/libmokomvvm_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 两页签验收页面
6.2 构建、签名和安装
.\gradlew.bat :mvvm-livedata:jvmTest :mvvm-state:jvmTest # 10 项交叉验证
.\gradlew.bat :example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64 # so 三件套回填
cd example\harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组,勾选自动签名后由 IDE 回填)。
hdc list targets -v
hdc -t <设备序列号> install -r \
entry/build/default/outputs/default/entry-default-signed.hap
hdc -t <设备序列号> shell aa start \
-a EntryAbility -b org.terminator.ohos.mokomvvm -m entry
四、完整代码对照
4.1 整体架构
mvvm-internal / mvvm-core / mvvm-flow / mvvm-livedata / mvvm-state / mvvm-test
│ 上游 commonMain + nonAndroidMain actual 原样编译
│ ViewModel / EventsDispatcher / LiveData / MediatorLiveData / CFlow / ResourceState
Bridge.kt + MiniJson
│ MokoMvvmDispatch / MokoMvvmVersion
libmokomvvm_napi.so
│ mokoMvvmDispatch / mokoMvvmVersion
Index.ets
│ JSON parse + @State 更新
▼
ArkUI 真机页面(两页签)
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_moko_mvvm、compose/android/resources/apple 模块移出 |
gradle/libs.versions.toml | Kotlin 2.4.20-0.1.0-07 + coroutines 双线 + moko-test 钉版 |
build.gradle.kts(根) | 版本 0.16.1-ohos.1、全局禁用 Ohos*Test* 任务 |
mvvm-*/build.gradle.kts ×6 | 纯 KMP:jvm + ohosArm64 + ohosX64、nonAndroidMain/nativeMain 层级重接、模块级 optIn |
mvvm-internal/src/ohosMain/.../runOnMainThread.kt | 唯一新增库源文件(12 行,内联执行) |
mvvm-flow/src/{commonMain,nonAndroidMain}/.../C{State,MutableState}Flow.kt ×4 | K2 模态修复:expect 显式声明 + actual 标记 |
example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
nativeApp/.../Bridge.kt | 2 个 C ABI、LoginViewModel Demo 域、runBlocking 会话、MiniJson、错误通道 |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 两页签验收页面 |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(22 节) |
*.upstream ×10 | 上游构建文件备份(审核对照用) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| 引擎 | ViewModel / viewModelScope / onCleared | 生命周期作用域与取消(nonAndroid actual) |
| 引擎 | EventsDispatcher.dispatchEvent / .listener | 弱引用监听者 + 未附加缓冲 + 主线程分发 |
| 引擎 | MutableLiveData.value / addObserver / removeObserver | 观察者语义(附加即回放当前值) |
| 引擎 | map / distinct / revert / mergeWith / List.all / mediator | mediator 变换链 |
| 引擎 | MediatorLiveData.addSource | 多源组合原语 |
| 引擎 | LiveData.asFlow / Flow.asLiveData | LiveData ↔ 协程互转(debounce 演示用) |
| 引擎 | CMutableStateFlow / CFlow.subscribe(scope, dispatcher) / DisposableHandle | 订阅包装与取消 |
| 引擎 | ResourceState / asState() / data() / error() | 加载态建模与派生链 |
| 测试 | TestViewModelScope.setupViewModelScope | scope 注入(上游测试机制,桥复用) |
| Native | MokoMvvmDispatch / MokoMvvmVersion | C ABI JSON 导出 |
| N-API | mokoMvvmDispatch(req) / mokoMvvmVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(op) | 触发操作并刷新 trace |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS:登录一步到位(先同步输入,再触发登录协程)
this.sendInput(VALID_USER, VALID_PASSWORD); // {"op":"input","sessionId":1,...}
const res = JSON.parse(mokoMvvmDispatch(
JSON.stringify({ op: 'login', sessionId: this.sessionId })));
// res.ok === true;res.before / res.after 两个快照对照,res.trace 逐行是引擎事件
// Kotlin:登录协程跑在注入的 viewModelScope 里
fun login() {
if (!isInputValid.value) { eventsDispatcher.dispatchEvent { showToast("invalid input...") }; return }
isLoading.value = true
loginState.value = ResourceState.Loading()
viewModelScope.launch {
delay(LOGIN_DELAY_MS) // 真实挂起与恢复
if (username.value == VALID_USER && password.value == VALID_PASSWORD) {
loginState.value = "token-1234".asState()
eventsDispatcher.dispatchEvent { routeToMainScreen(username.value ?: "") }
} else {
loginState.value = ResourceState.Failed("invalid credentials for '${username.value}'")
eventsDispatcher.dispatchEvent { showToast("login failed, check credentials") }
}
isLoading.value = false
}
}
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:真编译六个引擎模块而不是提取零依赖内核
早期仓库抽内核,是因为 ohos 上没有它们依赖的 kotlinx 制品。本次先验证 coroutines ohos 制品线存在,障碍只剩"版本主体"——再抽内核等于把 MVVM 引擎手抄一遍,反而引入漂移风险。真编译让 ViewModel 生命周期、事件缓冲、mediator 重算、订阅取消全部是上游原件。
决策 2:工具链跨代升级(Kotlin 1.8.10 → 2.4.20-0.1.0-07,Gradle 7.6.1 → 9.6.1)
moko-mvvm 是本批适配里上游工具链最老的库(2022 年钉版)。coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20 构建,旧线读不了 2.4 元数据;上游 wrapper 7.6.1 是 Kotlin 1.8 线的 workaround(KT-55751),换了工具链线即可解除,直接升 9.6.1。K2 随之带来的 expect/actual 收紧按"最小声明修复"处理(见决策 4),不为绕检查重写引擎。
决策 3:coroutines 双线,版本主体升到 1.11.0
上游钉 1.6.4(native-mt 时代),无 ohos 制品、无同体重建,社区线最早 1.10.2 / 1.11.0。取 1.11.0 主体:ohos 线 1.11.0-0.1.0-07(eazytec)、jvm 线 1.11.0(Maven Central 原版体)——jvm 交叉验证跑的正是 ohos .so 里执行的那一版行为,"换线不换语义"取最强形态。上游测试套件在 1.11.0 上零改动全绿,佐证 API 面无破坏。
决策 4:K2 模态修复用"显式声明 + actual 标记",而不是绕过
可选路径有三:重写 CFlow(弃 expect/actual)、隔离整个 mvvm-flow(损失 CFlow 能力)、或按 K2 规则补显式声明。第三种改动最小且签名与上游实现逐字相同——-Xexpect-actual-classes 实测无效,显式声明是社区迁移标准做法。4 个文件的改动在 README.OpenHarmony_CN.md §18 逐条留档。
决策 5:TestViewModelScope 注入,而不是碰 Dispatchers.Main
createUIDispatcher() = Dispatchers.Main 是上游原文,鸿蒙线 Main 跨线程派发有已知问题。桥不赌 Main 的行为:TestViewModelScope.setupViewModelScope 是上游 mvvm-test 为替换 scope 提供的 public API(上游单元测试就这么用),把 viewModelScope 指到 runBlocking 事件循环——协程时序确定性、线程纪律保持"全程不碰 Main",且用的完全是库自己的机制。
决策 6:jvmTest 交叉验证 + 测试零改动
引擎逻辑在 commonMain / nonAndroidMain,JVM 与 ohos 跑同一份代码;10 项 jvmTest 跑的就是上游原测试文件(moko-test 0.6.1 原坐标从 Maven Central 解析 jvm 变体),是"引擎没漂"的闸门。ohos 侧没有测试运行器,处置不是改测试,而是"测试统一 JVM + 根构建禁用 Ohos*Test*"——测试文件一个字节不动,交叉验证的有效性才成立。
六、测试与验证
6.1 测试环境
本次验证使用 Windows(x64)、JDK 21(Eclipse Adoptium)、Gradle 9.6.1、Kotlin/Native 鸿蒙线 2.4.20-0.1.0-07、DevEco Studio 5.1.0 Release(HAP / 签名 / 真机)与鸿蒙真机(arm64)。Gradle 侧(KLIB + KN so)不依赖 DevEco,C++ 层另有 ohos sysroot 真实头文件冒烟编译。
6.2 JVM 交叉验证
./gradlew :mvvm-livedata:jvmTest :mvvm-state:jvmTest
10 项用例全部通过(上游原测试文件,零改动):
| 测试类 | 用例数 | 钉住什么 |
|---|---|---|
mvvm-livedata LiveDataTest | 6 | LiveData 观察者语义、 mediator 组合、变换链 |
mvvm-livedata LiveDataFlowTest | 2 | asFlow / asLiveData 协程互转 |
mvvm-state LiveDataTest | 2 | ResourceState 流转与派生链 |
依赖换线没有 API 漂移:coroutines 1.6.4 → 1.11.0(jvm 线 Maven Central 原版体)后上游测试照跑全绿。上游 mvvm-core / mvvm-flow / mvvm-internal / mvvm-test 本就没有 commonTest 源集,jvmTest 对应 NO-SOURCE,属正常。
6.3 构建验证
:mvvm-*:compileKotlinOhosArm64 / OhosX64(六模块): BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
产物: libohosmokomvvm.so arm64 ≈5.4MB / x64 ≈4.9MB(debugShared,含 KN 运行时与 coroutines)
+ libc++_shared.so 双 ABI,copyKnSo* 自动回填 harmonyApp/entry/libs/<abi>/
C ABI 符号: MokoMvvmDispatch / MokoMvvmVersion(生成头文件逐一核对)
C++ 冒烟编译: clang --target=aarch64-linux-ohos,konan ohos sysroot 真实
napi/native_api.h + hilog/log.h,napi_init.cpp 编译通过
6.4 功能用例(真机验收点)
- 启动后标题行出现
moko-mvvm 0.16.1-ohos.1 (upstream 0.16.1) · kotlin 2.4.20-0.1.0-07 · coroutines 1.11.0-0.1.0-07(来自真实 Native 调用,非静态文字); - 创建会话:trace 出现 scope 注入说明、三个 observer 的首帧回调(上游契约 addObserver 即回放当前值)、
session created: id=1; - 填入无效凭据(guest/123):
observer<isInputValid>: true → false重算链可见,登录被拦,仅出现event:showToast(invalid input...); - 填入有效凭据(admin/12345678):
valid翻回 true; - 登录:trace 顺序呈现
isLoading=true → loginState=Loading → (25ms 真实协程)→ loginState=Success(data=token-1234) → event:routeToMainScreen(user=admin) → isLoading=false,before/after 快照对照; - 销毁:
onCleared: viewModelScope.isActive true → false;再点登录,trace 零新增——协程因 scope 取消未运行; - Engine 页签五连:eventsDispatcher 缓冲→回放→内联→再缓冲时序;livedata 的 observer 摘除静默与 mediator 重算;debounce 只收
d;flow 的DisposableHandle.dispose()后静默;state 的 data()/error() 派生链; - 每步操作的 trace 按引擎时序滚动留痕,
ok:false错误原文进日志区。
6.5 验证结论
MVVM 引擎六个模块、coroutines ohos 制品线、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的 MVVM 会话:版本握手、输入校验链、登录协程、事件分发与生命周期取消闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/moko-mvvm.git
cd moko-mvvm
export JAVA_HOME="/path/to/jdk-21" # Windows: $env:JAVA_HOME = "..."
./gradlew compileKotlinOhosArm64 compileKotlinOhosX64 # 六模块 klib
./gradlew :mvvm-livedata:jvmTest :mvvm-state:jvmTest # 10 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 \
:example:nativeApp:linkDebugSharedOhosX64 # so 三件套回填
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.mokomvvm -m entry
hdc shell hilog | grep MokoMvvm
八、遗留问题与改进方向
8.1 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让六个引擎模块真正编译成
ohosArm64/ohosX64动态库; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;kotlinx-coroutines-core在 Maven Central 上的 1.6.4 没有 ohos 变体,社区线最早 1.10.2 / 1.11.0——版本主体写错代,解析与编译都不会过;coroutines 1.11.0-0.1.0-07 钉版后 Kotlin 必须跟到 2.4.20-0.1.0-07(stdlib 元数据 2.4.0,2.2.21 线读不了);- 上游 Gradle 7.6.1 是 Kotlin 1.8 线的钉版 workaround(KT-55751 注释在 wrapper 里写着),换工具链线即可升 9.6.1——工具链经验要按线区分,不能把"上游钉 7.6.1"当成必须保留的事实;
- Kotlin 2(K2)对 expect/actual 类新增模态一致性检查:
CStateFlow/CMutableStateFlow报 “the modalities are different (‘abstract’ vs ‘open’)”;实测-Xexpect-actual-classes不能消除,必须 expect 显式声明 + actual 标记,上游 master 至今未迁移; kotlin.native.ref.WeakReference在鸿蒙线标注@ExperimentalNativeApi,且 opt-in 会沿 actual typealias 传导到引用方——全部库模块统一加模块级 optIn,源码零改动;kotlin-test-junit是 jvm/android 专属坐标,放在 mvvm-test 的 commonMain 会卡死 ohos 编译,换多平台坐标kotlin("test");CFlow.subscribe()无参重载写死GlobalScope + Dispatchers.Main(上游 nonAndroid actual 原文),鸿蒙线 Main 跨线程派发有已知问题——ohos 上必须用三参显式 scope 重载;List<LiveData<Boolean>>.all(value)的语义是"所有源都等于 value"而非"全部为真":校验链要写.all(true),写.all(false)会把校验结果整个反转(真机截图复核时发现并已修复);- 鸿蒙线 coroutines 把
Dispatchers.IO标为 internal(FFRT 调度模型下没有独立 IO 池)——本库恰好没用到 IO;mvvm-internal 的createUIDispatcher() = Dispatchers.Main是上游原文,桥经 TestViewModelScope 注入绕开,不赌 Main 行为; moko-test 0.6.1无 ohos 变体:测试统一 JVM 跑 + 根构建全局禁用Ohos*Test*任务,上游测试文件零改动;- KN 返回字符串必须
DisposeString归还运行时,N-API 层不能free();HAP 缺当前 ABI 的 so 会报安装失败9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可。
8.2 已知问题
- compose 适配模块(mvvm-compose / mvvm-flow-compose / mvvm-livedata-compose,上游钉 JetBrains Compose 1.3.1)不进构建,留盘为 P2;社区 compose ohos 线(1.12.0-0.1.0-07,orbit-compose 已验证)是后续路线的基础;
MutableLiveData.postValue/setValue(async)(上游 mobileMain,android+ios 专属概念)不进构建;ohos 消费者用.value = x(桥的线程纪律下二者等价);viewModelScope默认挂Dispatchers.Main(上游原文):主线程内联路径成立,跨线程派发未验证为可靠——桥一律经 TestViewModelScope 注入,属"记录而非绕过";EventsDispatcher的弱引用语义不跨桥演示(需要强制 GC 场景);- android / js / wasmJs / apple / linux / windows 目标剥离(源文件留在树里),发布、dokka、detekt 一并剥离,本 fork 不发 Maven Central;
- 单会话 Demo:
create覆盖式注入 scope,多会话并行属 Demo 层限制而非引擎限制。
8.3 未来优化方向
- P2:基于社区 compose / lifecycle ohos 线(1.12.0-0.1.0-07 / 2.11.0-0.1.0-07)重编 compose 适配模块,需逐文件评估 1.3.1 → 1.12 的 API 漂移;
- 若引入视图绑定层,把 mobileMain 的
postValue挂进 ohos 层级并补 actual; - 评估
.har形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
- 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游六个引擎模块(commonMain + nonAndroidMain actual)
→ 验证 coroutines ohos 制品线(1.11.0-0.1.0-07;上游 1.6.4 无制品 → 双线 + 主体升级)
→ 工具链跨代对齐 Kotlin 1.8.10→2.4.20-0.1.0-07、Gradle 7.6.1→9.6.1
→ 根构建替换(cmp_qrose 模式,build-logic 退出,层级重接三条边)
→ K2 expect/actual 修复(4 文件显式声明 + actual 标记)+ 唯一 12 行 ohos actual
→ ohosArm64 / ohosX64 KLIB + 双 ABI 动态库
→ C ABI(@CName × 2)+ 扁平 JSON + TestViewModelScope 会话
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析
→ ArkUI 真机两页签验收(创建 / 输入 / 登录 / 销毁 + 引擎五连)
9.2 封装层次
- 引擎层:上游原版
ViewModel(viewModelScope / onCleared)、EventsDispatcher(弱引用 + 缓冲)、LiveData/MediatorLiveData变换链、CFlow订阅包装、ResourceState; - 测试层:上游原版
TestViewModelScope/TestEventsDispatcher/TestObserver,桥与 jvm 测试共用同一套机制; - Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、会话 id、操作分发、trace 渲染与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 工具链跨代升级的钥匙是依赖线,不是上游现状:moko-mvvm 上游钉在 2022(Kotlin 1.8.10 / Gradle 7.6.1 / coroutines 1.6.4),三项全部随"coroutines ohos 线要求 stdlib 2.4.20"这一事实联动升级;上游的钉版注释(KT-55751)反而指明了哪条限制随工具链线解除;
- K2 收紧是新一批适配的共性问题,解法要最小化:expect/actual 模态检查挡住的是 2022 年写法的库(上游 master 都没迁移过);"expect 显式声明 + actual 标记"改动最小、签名逐字对齐上游实现,且全程留档可审——比重写或隔离模块都更可辩护;
- MVVM 类库的验收重点是"时序可见":存储库证明"重启后数据还在",MVVM 库要证明的是 observer 回调顺序、mediator 重算时机、事件缓冲与回放、协程取消——把引擎 trace 逐行回传到页面上,语义时序第一次直接可见,比断言式验收更有说服力。
9.4 适配成果
当前适配已完成:
- moko-mvvm
0.16.1(上游)真实引擎六个模块的双 ABI Kotlin/Native 编译(上游源文件零重写,仅 4 个声明文件按 K2 规则补标注 + 1 个 12 行 ohos actual); - HarmonyOS coroutines 制品线在 MVVM 库上的完整落地(ohos 线
1.11.0-0.1.0-07+ jvm 线1.11.0双线,commonMain 坐标不动); - 工具链跨代升级验证(Kotlin 1.8.10 → 2.4.20-0.1.0-07,Gradle 7.6.1 → 9.6.1,六模块 + 桥全部编译链接通过);
- 2 个 C ABI 符号和 C++ N-API 包装层(生成头文件符号核对 + ohos sysroot 真实头文件冒烟编译通过);
- DevEco Stage ArkUI 两页签验收页面(版本握手、登录会话四步流、引擎五连、trace 日志);
- 10 项 JVM 交叉验证全绿(上游原测试文件)+ 双 ABI
.so(arm64 ≈5.4MB / x64 ≈4.9MB)、HAP 构建、签名、设备安装和真机验证; - AtomGit 项目文档、OpenHarmony README(中英双份)、桥协议契约和本地效果图。
适配过程 FAQ
以下问题来自
ohosArm64目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。
Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?
- 确认 DevEco Studio 版本与工程
build-profile.json5的compatibleSdkVersion一致; - 在
File > Settings > Build, Execution, Deployment > Ohpm配置可用的 ohpm 仓库镜像或代理; - 删除
oh_modules、oh-package-lock.json5与.hvigor缓存后重新 Sync; - 公司网络需要代理时,同时给 ohpm 与 hvigor 配置
http.proxy。
Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?
- 先在
example目录执行./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用linkDebugSharedOhosX64); - 确认
libxxx.so与libc++_shared.so已复制到ohos/entry/libs/<abi>/; - 检查
entry/build-profile.json5的abiFilters与设备 ABI 是否一致(真机arm64-v8a,模拟器x86_64); - 清理
entry/.cxx与entry/build后重试。
Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?
- 确认 CMake 的
add_library(<target> SHARED ...)名称与import ... from 'libxxx_napi.so'完全一致; - 解包 HAP,确认
libs/<abi>/libxxx_napi.so已打包; - 删除
entry/build、entry/.cxx与oh_modules后重新构建; - 检查
module.json5的srcEntry与页面路径是否正确。
Q4:真机安装报 signature verification failed 或 install failed?
- 在 DevEco Studio 的
Signing Configs配置证书、Profile 与密码; bundleName必须与签名 profile 中的 bundle name 一致;- 设备需加入 profile 的
deviceIds,调试证书有效期需覆盖当前时间; - 用
hdc list targets确认设备在线,再执行hdc install -r xxx-signed.hap。
Q5:应用启动白屏、闪退或 NAPI 调用崩溃?
- 用
hdc hilog | grep -i "napi\|kotlin"查看原生日志; - 确认
libc++_shared.so与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致; - 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用
XxxFree释放; - 在 DevEco 的 Native 调试配置中附加
lldb,定位空指针与越界访问。
Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?
- 使用鸿蒙发行版 Kotlin(如
2.2.21-1.0.0),并在settings.gradle.kts配置 eazytec 仓库; - Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
- 确认
kotlin { ohosArm64() }已应用,且local.properties的 SDK 路径有效; - 清理
~/.gradle/caches中对应版本的 Kotlin/Native 缓存后重试。
Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?
- 对齐 DevEco Studio、HarmonyOS SDK 与
build-profile.json5中的compatibleSdkVersion; - 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免
modelVersion与 IDE 不匹配; - 命令行构建时设置
DEVECO_SDK_HOME指向 DevEco 的sdk目录。
Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?
- 使用 DevEco 自带的 node 与 hvigor:
"$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...; - 设置
JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME; - 加上
--no-daemon排除守护进程缓存影响,并清理entry/build与.hvigor。
参考文档
更多推荐



所有评论(0)