本文记录 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 / collectedeventsDispatcher 送达时序 / 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 MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向 Eclipse Adoptium)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.6.1(上游 wrapper 7.6.1 升级;上游因 KT-55751 钉 7.6.1,该限制只属于 Kotlin 1.8 线)
kotlinxcoroutines ohos 线 1.11.0-0.1.0-07 / jvm 线 1.11.0(双线,见 2.1)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.mokomvvm
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
模块commonMain 依赖平台源集处置
mvvm-internalcoroutinesnonAndroid / native / apple / js / jvm / linux / mingw整体编译(jvm + 双 ohos)
mvvm-coremvvm-internal + coroutines(androidMain 另有 androidx.lifecycle)common expect + android / nonAndroid actual整体编译(androidMain 留盘不编)
mvvm-flowmvvm-core + kswift-runtimecommon expect + android / ios / nonAndroid actual整体编译(kswift 依赖随 iosMain 一起剥离;4 文件 K2 标注)
mvvm-livedatamvvm-core + kswift-runtime(androidMain 另有 androidx livedata)common + nonAndroid + android + ios + mobileMain整体编译(mobileMain / androidMain / iosMain 留盘)
mvvm-statemvvm-livedata100% commonMain整体编译(零平台源码)
mvvm-testmvvm-internal + mvvm-core + kotlin-testcommon + 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 方法覆盖内核
MokoMvvmDispatchmokoMvvmDispatch(req)会话四步 + 引擎五连,共十个 op
MokoMvvmVersionmokoMvvmVersion()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.tomlKotlin 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 ×4K2 模态修复:expect 显式声明 + actual 标记
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt2 个 C ABI、LoginViewModel Demo 域、runBlocking 会话、MiniJson、错误通道
harmonyApp/.../cpp/napi_init.cppN-API 导出、参数检查、DisposeString
harmonyApp/.../cpp/CMakeLists.txtIMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套
harmonyApp/.../ets/pages/Index.etsArkUI 两页签验收页面
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 / mediatormediator 变换链
引擎MediatorLiveData.addSource多源组合原语
引擎LiveData.asFlow / Flow.asLiveDataLiveData ↔ 协程互转(debounce 演示用)
引擎CMutableStateFlow / CFlow.subscribe(scope, dispatcher) / DisposableHandle订阅包装与取消
引擎ResourceState / asState() / data() / error()加载态建模与派生链
测试TestViewModelScope.setupViewModelScopescope 注入(上游测试机制,桥复用)
NativeMokoMvvmDispatch / MokoMvvmVersionC ABI JSON 导出
N-APImokoMvvmDispatch(req) / mokoMvvmVersion()向 ArkTS 暴露能力
ArkTSdispatch(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 LiveDataTest6LiveData 观察者语义、 mediator 组合、变换链
mvvm-livedata LiveDataFlowTest2asFlow / asLiveData 协程互转
mvvm-state LiveDataTest2ResourceState 流转与派生链

依赖换线没有 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 功能用例(真机验收点)

  1. 启动后标题行出现 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 调用,非静态文字);
  2. 创建会话:trace 出现 scope 注入说明、三个 observer 的首帧回调(上游契约 addObserver 即回放当前值)、session created: id=1;
  3. 填入无效凭据(guest/123):observer<isInputValid>: true → false 重算链可见,登录被拦,仅出现 event:showToast(invalid input...);
  4. 填入有效凭据(admin/12345678):valid 翻回 true;
  5. 登录:trace 顺序呈现 isLoading=true → loginState=Loading → (25ms 真实协程)→ loginState=Success(data=token-1234) → event:routeToMainScreen(user=admin) → isLoading=false,before/after 快照对照;
  6. 销毁:onCleared: viewModelScope.isActive true → false;再点登录,trace 零新增——协程因 scope 取消未运行;
  7. Engine 页签五连:eventsDispatcher 缓冲→回放→内联→再缓冲时序;livedata 的 observer 摘除静默与 mediator 重算;debounce 只收 d;flow 的 DisposableHandle.dispose() 后静默;state 的 data()/error() 派生链;
  8. 每步操作的 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 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让六个引擎模块真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 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 线读不了);
  4. 上游 Gradle 7.6.1 是 Kotlin 1.8 线的钉版 workaround(KT-55751 注释在 wrapper 里写着),换工具链线即可升 9.6.1——工具链经验要按线区分,不能把"上游钉 7.6.1"当成必须保留的事实;
  5. Kotlin 2(K2)对 expect/actual 类新增模态一致性检查:CStateFlow / CMutableStateFlow 报 “the modalities are different (‘abstract’ vs ‘open’)”;实测 -Xexpect-actual-classes 不能消除,必须 expect 显式声明 + actual 标记,上游 master 至今未迁移;
  6. kotlin.native.ref.WeakReference 在鸿蒙线标注 @ExperimentalNativeApi,且 opt-in 会沿 actual typealias 传导到引用方——全部库模块统一加模块级 optIn,源码零改动;
  7. kotlin-test-junit 是 jvm/android 专属坐标,放在 mvvm-test 的 commonMain 会卡死 ohos 编译,换多平台坐标 kotlin("test");
  8. CFlow.subscribe() 无参重载写死 GlobalScope + Dispatchers.Main(上游 nonAndroid actual 原文),鸿蒙线 Main 跨线程派发有已知问题——ohos 上必须用三参显式 scope 重载;
  9. List<LiveData<Boolean>>.all(value) 的语义是"所有源都等于 value"而非"全部为真":校验链要写 .all(true),写 .all(false) 会把校验结果整个反转(真机截图复核时发现并已修复);
  10. 鸿蒙线 coroutines 把 Dispatchers.IO 标为 internal(FFRT 调度模型下没有独立 IO 池)——本库恰好没用到 IO;mvvm-internal 的 createUIDispatcher() = Dispatchers.Main 是上游原文,桥经 TestViewModelScope 注入绕开,不赌 Main 行为;
  11. moko-test 0.6.1 无 ohos 变体:测试统一 JVM 跑 + 根构建全局禁用 Ohos*Test* 任务,上游测试文件零改动;
  12. 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 三条经验

  1. 工具链跨代升级的钥匙是依赖线,不是上游现状:moko-mvvm 上游钉在 2022(Kotlin 1.8.10 / Gradle 7.6.1 / coroutines 1.6.4),三项全部随"coroutines ohos 线要求 stdlib 2.4.20"这一事实联动升级;上游的钉版注释(KT-55751)反而指明了哪条限制随工具链线解除;
  2. K2 收紧是新一批适配的共性问题,解法要最小化:expect/actual 模态检查挡住的是 2022 年写法的库(上游 master 都没迁移过);"expect 显式声明 + actual 标记"改动最小、签名逐字对齐上游实现,且全程留档可审——比重写或隔离模块都更可辩护;
  3. 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。

参考文档

Logo

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

更多推荐