本文记录 SimpleMVI 接入 OpenHarmony 的完整过程,覆盖状态容器库盘点、双模块(core + compose)真编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

SimpleMVI 是基于 kotlinx.coroutines 的 Kotlin Multiplatform MVI 状态容器库:StateUi / IntentUi / EffectUi 三个标记接口给出显式契约,MviStore / createStore / BaseMviStore 提供可观察状态、一次性效果与原子状态更新,MviConfig 挂上进程级全局观测钩子,compose 模块再用 MviViewModel(继承 androidx.lifecycle.ViewModel)与 CollectUiEffects 把引擎接进 Compose 世界。本次适配与社区早期路线(qrose / haze / PreCompose / voyager 的"隔离 Compose、零依赖内核提取")有本质区别:社区 Maven 上不但有 kotlinx 制品线,Compose runtime 与 JetBrains multiplatform lifecycle 的 ohos 制品线也已补齐(compose 1.12.0-0.1.0-07、lifecycle 2.11.0-0.1.0-07,全部带 ohosArm64 / ohosX64 Gradle 变体),因此不需要隔离也不需要提取——core 与 compose 两个模块的全部 commonMain 原样编译成 OpenHarmony KLIB(零 expect/actual,全仓没有一行新增 Kotlin 源文件),一个带状态机、效果缓冲、全局观测的完整 MVI 引擎运行在真机上,ArkUI 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/SimpleMVI.git

上游仓库: v1rus-dev/SimpleMVI

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配

SimpleMVI 为 Kotlin Multiplatform 应用提供"状态容器"这一件事的完整答案:createStore(initialState) 一行建店,onIntent / updateState / emitEffect / tryEmitEffect 覆盖全生命周期,uiState: StateFlow 让 UI 订阅状态、uiEffects: Flow 承接一次性效果(默认缓冲 1 + DROP_OLDEST),BaseMviStore 保证"先通知全局钩子、再处理意图"的顺序,MviConfig.configure 用合并语义挂 onIntent / onEffect / onError 三个进程级钩子。compose 模块在此之上给出 MviViewModel(委托 MviStore 的 ViewModel 基类)与生命周期门控的 CollectUiEffects。KMP 项目把这些能力放在 commonMain,由 Android、iOS 共享。OpenHarmony 应用同样需要这套状态管理能力,但上游构建(Kotlin 2.3.21 / Gradle 8.14.4 / AGP 8.13.2 的新版 com.android.kotlin.multiplatform.library 插件)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明公共 MVI 引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态引擎、coroutines 运行时、compose 模块的 ViewModel 包装、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.3.21 / Gradle 8.14.4 / AGP 8.13.2 与鸿蒙工具链无法共存于一个 Gradle 构建。
依赖矩阵core 依赖 kotlinx-coroutines,compose 模块还要 org.jetbrains.compose.runtime:runtime 与 org.jetbrains.androidx.lifecycle 两件套——每一个都要有 ohos 变体才能解析。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
状态语义跨不了桥StateFlow / SharedFlow 订阅、挂起 emitEffect 是 Kotlin 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:双模块真实引擎的 ohos 编译(core 的 MviStore 引擎 + compose 的 MviViewModel 包装)、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。MVI 语义完全由上游原版实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

Demo 提供一个真实的 MVI 会话,对应桥的全部入口:

  • 建会话(create):同一初始值同步创建两个 target——core 的 BaseMviStore 子类与 compose 的 MviViewModel 子类,并安装记录型 MviConfig 钩子;
  • 意图(intent):increment / decrement / add(N) / reset / announce(text) 五种意图,onIntent 先通知全局钩子、再执行共享的意图处理;
  • 状态(state):uiState.value 快照,含 count、operations、lastIntent;
  • 效果(effects):count 从 <10 越到 ≥10 触发 ThresholdReached(10)(tryEmit 通道),reset 触发 ResetPerformed,announce 走挂起 emitEffect(suspend 通道)——两条发射通道都有真机验收;
  • 全局观测(config):钩子记录按 intent / effect / error 分色展示;configThrow 把 onIntent 换成抛异常钩子,验证"观测永不破坏 store"的上游契约;configReset 恢复 no-op;
  • 版本握手(version):simpleMviVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / target回显本次操作与目标 store(store = BaseMviStore,viewmodel = MviViewModel)。
state分发后的新状态:count / operations / lastIntent。
deliveredEffects本次 intent 派发期间 collector 实际收到的一次性效果数。

引擎侧完整能力(原子 updateState、replay=0 的效果流、DROP_OLDEST 缓冲、MviConfig 合并语义与异常隔离)全部在上游 commonMain 里原样随编译进入 .so,JVM 侧保留 25 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的状态逻辑没有漂移。

1.3 实现适配

维度要求
代码复用simple-mvi-core 与 simple-mvi-compose 的全部 commonMain 文件原样编译,不提取、不重写、不隔离;全库零 expect/actual,ohos 侧一行新增 Kotlin 源码都不需要。
平台目标根构建为两个库模块声明 jvm + ohosArm64 + ohosX64(jvm 作交叉验证),example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);simple-mvi-android(纯 AGP)与两个 samples 留盘不进 settings。
桥接稳定使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律jvm 交叉验证线与上游逐字相同(换线不换版);ohos 线取社区已发布制品体(coroutines 1.11.0 / compose 1.12.0 / lifecycle 2.11.0),上游版本体无 ohos 构建,这是制品可达性决定的。
UI 完整单页三页签覆盖全部桥接入口:会话(状态机) / 观测(MviConfig) / 关于(握手与分层)。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。OHOS 尚无 Compose 宿主,CollectUiEffects 只能编译验证(klib);ArkTS Demo 把桥暴露的 MVI 会话画出来,MviViewModel 则以无 UI 实例化的方式在真机执行——这正是"compose 模块也真编译"的验收方式。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 ohos 制品线(coroutines / compose / lifecycle)、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.4.20-0.1.0-07 钉版、依赖按源集分线
第 3 阶段:桥协议建模     ── 扁平 JSON 载荷、8 个 op 的请求/响应契约、双 target 会话、错误通道
第 4 阶段:原生桥接       ── @CName C ABI、runBlocking 纪律、派发期订阅、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 三页签验收页、版本握手、intent 按钮、效果与观测日志
第 6 阶段:示例与验证     ── jvm 交叉验证、HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
simple-mvi-core/                   上游状态引擎(进 ohos 构建,jvm + ohosArm64 + ohosX64)
simple-mvi-compose/                上游 compose 集成(进 ohos 构建,MviViewModel / CollectUiEffects 原样编译)
simple-mvi-android/                SavedStateHandle 胶水(纯 AGP),留盘不进 settings
samples/                           上游 Android / iOS 示例,留盘不进 settings
example/nativeApp/                 Kotlin/Native 动态库(Bridge + MiniJson,驱动 core 与 compose 双模块)
example/harmonyApp/                DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md     鸿蒙适配说明(中英双份)
README.md / README.upstream.md     本 fork 中文首页 / 上游原文保存
*.upstream                         上游构建脚本备份(settings / build / gradle.properties / catalog / 模块 build)
docs/                              博客与效果图

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :simple-mvi-core、:simple-mvi-compose 与 :example:nativeApp,rootProject.name = "ohos_simplemvi"。上游的 vanniktech 发布插件、AGP classpath、新版 KMP android 插件(com.android.kotlin.multiplatform.library)全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.4.20-0.1.0-07(eazytec Maven)
JDK21(org.gradle.java.home 指向本机 JDK 21)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作交叉验证)
Gradle9.6.1(上游 wrapper 8.14.4 升级;2.4.20 线支持 Gradle 9.x)
kotlinxcoroutines / coroutines-test 1.11.0-0.1.0-07(ohos 制品线)
Composeruntime 1.12.0-0.1.0-07(ohos 制品线,jvm 线用上游体 1.10.3)
lifecycleviewmodel-compose / runtime-compose 2.11.0-0.1.0-07(ohos 制品线,jvm 线用上游体 2.10.0)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.simplemvi
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
1.3 源码依赖度盘点
模块commonMain 文件依赖形态处置
simple-mvi-core6纯 Kotlin + coroutines,零 UI、零 AGP整体编译(jvm + 双 ohos)
simple-mvi-compose2compose runtime + org.jetbrains.androidx.lifecycle(ViewModel / repeatOnLifecycle)整体编译(ohos 线已有全部制品)
simple-mvi-android1androidx.lifecycle.SavedStateHandle,Android 专属留盘不进构建
samples多AGP / iOS 工具链留盘不进构建

expect 声明全库 0 处——core 引擎只有纯 Kotlin 与 coroutines,compose 模块的 ViewModel 也不碰平台 API。对比 KStore 适配还需要补 8 行 ohos dispatcher actual,SimpleMVI 是这批 CMP 库里适配面最小的一类:ohos 侧连一行新增 Kotlin 源文件都没有,全部工作在构建脚本与桥接层。

simple-mvi-compose 值得单独说明:早期路线(qrose 时代)到这里只能把 Compose 依赖代码隔离进不编译的 composeMain;本次适配首先验证了社区 Maven 上 org.jetbrains.compose.runtime:runtime 1.12.0-0.1.0-07 与 org.jetbrains.androidx.lifecycle:* 2.11.0-0.1.0-07 的 .module 元数据均含 ohosArm64ApiElements-published / ohosX64ApiElements-published 变体——障碍已消失,MviViewModel 原样编译进 .so 并在真机执行。

1.4 页面边界
标题区          SIMPLEMVI · OPENHARMONY / 版本握手行(KMP MVI 状态容器 · 0.4.0-ohos.1 · core + compose 双模块真编译)
Tab 切换        会话 / 观测 / 关于
目标区          BaseMviStore · core ↔ MviViewModel · compose 双 chip 切换(同一 intent 逻辑、两条编译路径)
状态卡          count 大数字(越过阈值 10 变主色)/ operations / lastIntent / 状态行
操作区          −1 / +1 / 重置 三色按钮、+N(数字输入)、Announce(文本输入,走挂起 emitEffect)
会话区          初始值输入 + 重建会话按钮
效果区          collector 已投递的一次性效果列表(seq + 效果名)
日志区          带时间戳的操作日志(→ 请求 / ← 响应,最新在前,上限 100 条)
观测区          MviConfig 钩子记录(intent/effect/error 分色)、读取并清空、抛异常钩子、重置配置
关于区          版本握手、库简介、架构分层、仓库与社区链接(AtomGit)

页面启动时先调 simpleMviVersion() 做握手,再自动 create 建立会话。

第 2 阶段:目标与依赖打通

2.1 制品线验证(本适配的成败手)

先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 ohos 的三件套制品。用 .module 元数据逐个验证:

org.jetbrains.kotlinx:kotlinx-coroutines-core:1.11.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published / jvmApi-published
org.jetbrains.compose.runtime:runtime:1.12.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published(无 *-desktop 子制品)
org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose:2.11.0-0.1.0-07
  variants: ohosArm64ApiElements-published / ohosX64ApiElements-published / desktopApiElements-published
org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose:2.11.0-0.1.0-07
  variants: 同上

三个关键结论:其一,lifecycle ohos 线的补齐让 compose 模块整库编译成为可能——这是本次适配与 qrose 时代的分水岭,MviViewModel(androidx ViewModel 子类)不必再被隔离;其二,compose ohos 线不发布 jvm 目标需要的 *-desktop 子制品,所以 jvm 交叉验证线必须从 Maven Central 取上游原版版本体(compose 1.10.3 / lifecycle 2.10.0),依赖要按源集分线声明;其三,coroutines 1.11.0-0.1.0-07 与 KStore 适配用的是同一条线,钉版经验直接复用。

2.2 工具链钉版(为什么是 2.4.20-0.1.0-07)

社区早期批准的钉法是 HarmonyOS Kotlin 2.2.21-1.0.0(qrose / haze 用的)。但 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。额外好处:上游源码本来就是 Kotlin 2.3.21 编的,2.4.20 与之同代,源码级兼容成本为零;Gradle 升到 9.6.1(2.4.20 线支持 Gradle 9.x,KStore / capsule 先例),不像 2.2.21 旧线必须钉 8.14.x。

settings.gradle.kts 把 eazytec 仓库写在最前:

pluginManagement {
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        gradlePluginPortal()
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_simplemvi"

include(":simple-mvi-core")
include(":simple-mvi-compose")
include(":example:nativeApp")
2.3 编译真实引擎

simple-mvi-core/build.gradle.kts 收敛为纯 KMP 模块,coroutines 按源集分线:

kotlin {
    applyDefaultHierarchyTemplate()
    jvm { compilerOptions { jvmTarget.set(JvmTarget.JVM_11) } }
    ohosArm64()
    ohosX64()

    sourceSets {
        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation(libs.kotlinx.coroutines.test)   // 仅 JVM 测试解析,见 2.4
            implementation(libs.koin.core)
        }
        ohosMain.dependencies { api(libs.kotlinx.coroutines.core) }          // 1.11.0-0.1.0-07
        jvmMain.dependencies  { api(libs.kotlinx.coroutines.core.jvm) }      // 1.10.2(上游原版体)
    }
}

simple-mvi-compose 同构,多两件事:compose 编译器插件(来自 HarmonyOS Kotlin 线,CollectUiEffects 里有 @Composable)与 compose / lifecycle 的双线声明:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
    alias(libs.plugins.kotlin.compose.compiler)   // org.jetbrains.kotlin.plugin.compose 2.4.20-0.1.0-07
}

kotlin {
    jvm(); ohosArm64(); ohosX64()
    sourceSets {
        commonMain.dependencies { api(project(":simple-mvi-core")) }
        ohosMain.dependencies {
            implementation(libs.compose.runtime)                  // 1.12.0-0.1.0-07
            implementation(libs.lifecycle.viewmodel.compose)      // 2.11.0-0.1.0-07
            implementation(libs.lifecycle.runtime.compose)
        }
        jvmMain.dependencies {
            implementation(libs.compose.runtime.jvm)              // 1.10.3(上游原版体)
            implementation(libs.lifecycle.viewmodel.compose.jvm)  // 2.10.0(上游原版体)
            implementation(libs.lifecycle.runtime.compose.jvm)
        }
    }
}

实测:

./gradlew :simple-mvi-core:compileKotlinOhosArm64 :simple-mvi-core:compileKotlinOhosX64          # BUILD SUCCESSFUL
./gradlew :simple-mvi-compose:compileKotlinOhosArm64 :simple-mvi-compose:compileKotlinOhosX64    # BUILD SUCCESSFUL

两个模块的全部 commonMain 文件一个没动,ohos 侧零新增源码。

2.4 测试依赖的处置:koin-core 没有 ohos 制品

commonTest 的依赖表对 ohos 测试编译同样可见,koin-core 解析不到 ohos 变体就会卡死 compileTestKotlinOhosArm64。处置是两条纪律的组合:测试统一在 JVM 跑(jvmTest,coroutines-test / koin-core 走 Maven Central 上游体),根构建全局禁用 ohos 测试任务:

subprojects {
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

这样上游测试文件(含 Koin 用例)零改动保留,ohos 侧只承担编译与链接。

第 3 阶段:桥协议建模

3.1 JSON 契约
io.github.v1rusdev.simplemvi.core/**(上游引擎,原样编译)
        │ MviStore / createStore / BaseMviStore / MviConfig
io.github.v1rusdev.simplemvi.compose/**(上游 compose 集成,原样编译)
        │ MviViewModel(androidx.lifecycle.ViewModel 子类)
Bridge.kt(@CName × 2,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API simpleMviVersion() / simpleMviDispatch()
        ↓
ArkUI Index.ets(三页签滚动)

请求与响应:

请求 : {"op":"create","initial":0}
       {"op":"intent","target":"store|viewmodel","name":"increment|decrement|add|reset|announce","value":N,"text":"..."}
       {"op":"state","target":"..."}   {"op":"effects","target":"...","drain":true|false}
       {"op":"config","drain":true|false}   {"op":"configThrow","enable":true|false}
       {"op":"configReset"}   {"op":"version"}
响应 : {"ok":true,"op":"intent","target":"store","intent":"Add(5)","deliveredEffects":1,
        "state":{"target":"store","count":5,"operations":1,"lastIntent":"Add(5)"}}
       {"ok":false,"op":"intent","error":"unsupported intent name (...)"}

请求只支持扁平 JSON(MiniJson 纪律,继承 qrose / krogress / capsule):桥不引入 kotlinx.serialization,嵌套请求一律拒绝;响应里的数组(效果列表、钩子记录)由桥手工拼装,ArkTS 一次 JSON.parse 到位。

3.2 双 target 会话与订阅时机

桥内每个会话持有两个 target——core 的 BaseMviStore 子类与 compose 的 MviViewModel 子类,共享同一份意图处理逻辑:同一条 intent 在两条编译路径上必须跑出同样的状态迁移。MviViewModel 不需要 ViewModelStore 即可实例化(对桥会话足够),CollectUiEffects 无宿主则只做编译验证。

效果流有个关键语义:uiEffects 是 replay=0 的 SharedFlow,新订阅者看不到历史效果,所以订阅必须发生在派发之前。桥用上游 commonTest 同款模式解决:

runBlocking {
    val subscription = launch(start = CoroutineStart.UNDISPATCHED) {
        target.uiEffects.collect { effect -> received.add(EffectEntry(effect, nextSeq())) }
    }
    target.onIntent(intent)   // 先订阅,后派发;onIntent 先通知 MviConfig 再处理
    yield(); yield()          // 让 runBlocking 事件循环投递排队的效果
    subscription.cancel()
}

CoroutineStart.UNDISPATCHED 让订阅协程在当前线程内联执行到第一个挂起点,派发后两轮 yield() 让事件循环把效果投递给 collector——全程单线程,不需要任何锁(synchronized 在 Kotlin/Native 上本就不可用)。suspend 的 emitEffect(announce 路径)用 runBlocking 收进同步 C ABI:桥全程不碰 Dispatchers.Main(鸿蒙线跨线程派发有已知问题)。

3.3 错误通道

每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:

@CName("SimpleMviDispatch")
fun simpleMviDispatch(request: String): String {
    var op = ""
    return try {
        val json = MiniJson.parse(request)
        op = json.str("op")
        when (op) { /* 8 个 op 分发 */ }
    } catch (t: Throwable) {
        """{"ok":false,"op":${jsonString(op)},"error":${jsonString(t.message ?: t.toString())}}"""
    }
}

非法 JSON、缺 op / name、不支持的 op、未建会话就发 intent,全部走同一个 ok:false 通道,ArkTS 侧统一渲染。

第 4 阶段:原生桥接(技术难点)

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libsimplemvi_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohossimplemvi.so)
  │ SimpleMviDispatch / SimpleMviVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制 MVI 语义,所以选择 C ABI + JSON。

4.2 Kotlin/Native 导出函数
@CName("SimpleMviDispatch")
fun simpleMviDispatch(request: String): String   // 8 个 op 一口进

@CName("SimpleMviVersion")
fun simpleMviVersion(): String = "0.4.0-ohos.1"  // 版本握手
C ABI 符号ArkTS 方法覆盖内核
SimpleMviDispatchsimpleMviDispatch(req)建会话 + onIntent / updateState / tryEmitEffect / 挂起 emitEffect / MviConfig 全套
SimpleMviVersionsimpleMviVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"simpleMviDispatch", nullptr, NapiSimpleMviDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"simpleMviVersion", nullptr, NapiSimpleMviVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohossimplemvi_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free()。libohossimplemvi_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号),不拖整份生成头。

4.4 CMake 和类型声明
add_library(ohossimplemvi SHARED IMPORTED)
set_target_properties(ohossimplemvi PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(simplemvi_napi SHARED napi_init.cpp)
target_link_libraries(simplemvi_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohossimplemvi)

OHOS_ARCH 为空或 libohossimplemvi.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。

export const simpleMviDispatch: (request: string) => string;
export const simpleMviVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 握手和触发操作
aboutToAppear(): void {
  this.version = simpleMviVersion();   // 版本握手:来自真实 Native 调用
  this.loadVersion();
  this.createSession();                // 自动建会话(store + viewModel 双 target)
}

private sendIntent(name: string, value?: number, text?: string): void {
  let req: string = '{"op":"intent","target":"' + this.target + '","name":"' + name + '"' + /* ... */ + '}';
  const r = JSON.parse(simpleMviDispatch(req)) as IntentResp;
  if (r.ok) {
    this.applyState(r.state);          // @State 刷新状态卡
    this.refreshEffects(false);        // 拉取已投递效果
  }
}
5.2 页面交互

会话页签自上而下:目标双 chip(BaseMviStore · core / MviViewModel · compose)、状态卡(count 大数字,越过阈值 10 变主色)、三色意图按钮(+1 深绿 / −1 蓝 / 重置红)、+N 数字输入、Announce 文本输入、重建会话行、已投递效果列表与时间戳操作日志(→ 请求行 / ← 响应行,最新在前、上限 100 条)。观测页签展示 MviConfig 钩子记录(intent 蓝 / effect 绿 / error 红分色),提供刷新、读取并清空、安装抛异常钩子、恢复记录钩子、重置配置五个动作,并给出三条验收要点提示。关于页签展示版本握手行、ArkTS → libsimplemvi_napi.so → C ABI → libohossimplemvi.so → core/compose KLIB 分层说明与 AtomGit 仓库 / 社区链接。

5.3 对比度和错误边界

主操作深绿按钮白字,读取蓝、删除红,语义与破坏性对齐;ok:false 时错误原文进状态行与日志。观测页签的"抛异常钩子"是本 Demo 特有的验收面:安装后回会话页发 intent,状态必须照常更新——全局观测永不破坏 store 的上游契约在真机上直接可见。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohossimplemvi)+ copyKnSo*
│   └── src/ohosMain/kotlin/example/nativeapp/
│       └── Bridge.kt                         @CName × 2 + MiniJson(扁平 JSON,继承 cmp_qrose 审核模式)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.simplemvi、versionName 0.4.0-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/libohossimplemvi_api.h
        ├── src/main/cpp/types/libsimplemvi_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 三页签验收页面
6.2 构建、签名和安装
./gradlew :simple-mvi-core:jvmTest :simple-mvi-compose:jvmTest            # 25 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

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 回填带 material 的完整配置)。

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.simplemvi -m entry

四、完整代码对照

4.1 整体架构

io.github.v1rusdev.simplemvi.core/**(上游引擎原样编译)
    │ MviStore / createStore / BaseMviStore / updateState / tryEmitEffect / emitEffect
io.github.v1rusdev.simplemvi.compose/**(上游 compose 集成原样编译)
    │ MviViewModel(androidx.lifecycle.ViewModel 子类,无 UI 实例化驱动)
Bridge.kt + MiniJson
    │ SimpleMviDispatch / SimpleMviVersion
libsimplemvi_napi.so
    │ simpleMviDispatch / simpleMviVersion
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(三页签滚动)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_simplemvi、android/samples 移出
gradle/libs.versions.tomlKotlin 2.4.20-0.1.0-07 + 双制品线坐标(ohos 线 / jvm 上游体线)
build.gradle.kts(根)全局禁用 Ohos*Test* 任务
simple-mvi-core/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,coroutines 分线
simple-mvi-compose/build.gradle.kts同上 + compose 编译器插件,compose / lifecycle 分线
example/nativeApp/build.gradle.kts双 ABI sharedLib、copyKnSo* 回填三件套
nativeApp/.../Bridge.kt2 个 C ABI、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鸿蒙适配主文档(18 节)
*.upstream上游构建脚本备份(逐文件可对照)

4.3 关键 API 对照

层次API作用
引擎createStore(initialState) / BaseMviStore(initialState)建店(默认效果缓冲 1 + DROP_OLDEST)
引擎MviStore.onIntent(intent)先通知 MviConfig 钩子、再交给 handleIntent(桥不绕过该顺序)
引擎MviStore.updateState { }原子更新并返回新状态
引擎MviStore.tryEmitEffect(effect)try 通道;被缓冲拒绝时不通知 onEffect(上游语义)
引擎MviStore.emitEffect(effect)(suspend)挂起通道,runBlocking 收口
引擎MviStore.uiState / uiEffects状态快照 / replay=0 效果流(桥派发期订阅)
观测MviConfig.configure { onIntent / onEffect / onError }进程级钩子、合并语义、异常隔离
观测MviConfig.reset()恢复 no-op
composeMviViewModel(initialState) / sendEffectViewModel 包装,target=viewmodel 的全部 intent op
NativeSimpleMviDispatch / SimpleMviVersionC ABI JSON 导出
N-APIsimpleMviDispatch(req) / simpleMviVersion()向 ArkTS 暴露能力
ArkTSsendIntent(name)触发意图并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const r = JSON.parse(simpleMviDispatch('{"op":"intent","target":"viewmodel","name":"increment"}')) as IntentResp;
// r.ok === true;r.state.count 已 +1,r.deliveredEffects 为本次投递的效果数
val target = session.target("viewmodel")   // MviViewModel 子类
runBlocking {
    val subscription = launch(start = CoroutineStart.UNDISPATCHED) {
        target.uiEffects.collect { received.add(EffectEntry(it, nextSeq())) }
    }
    target.onIntent(DemoIntent.Increment)  // MviConfig 先收到钩子通知
    yield(); yield()
    subscription.cancel()
}

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。

五、关键决策说明

决策 1:真编译双模块而不是隔离 Compose

早期仓库隔离 Compose 依赖代码,是因为 ohos 上没有对应制品。本次适配先验证制品线(coroutines / compose runtime / lifecycle 全部存在 ohos 变体),障碍已消失——再隔离等于把 MviViewModel 排除在验收之外。真编译让状态机、效果缓冲、MviConfig 异常隔离全部是上游原件,且 compose 模块的 ViewModel 路径第一次跑进了鸿蒙真机。

决策 2:工具链升到 Kotlin 2.4.20-0.1.0-07,Gradle 升 9.6.1

coroutines 1.11.0-0.1.0-07 基于 stdlib 2.4.20 构建,旧钉法 2.2.21 读不了 2.4 元数据;上游本来就是 Kotlin 2.3.21,2.4.20 同代零降级。Gradle 方面 2.4.20 线支持 9.x(KStore / capsule 先例),上游 wrapper 8.14.4 升到 9.6.1——只有 2.2.21 旧线才必须钉 8.14.x,两条工具链线的经验不能互相套用。

决策 3:依赖按源集分线(ohosMain / jvmMain)

compose ohos 线不发布 jvm 目标需要的 *-desktop 子制品,单线声明必然有一侧解析失败。处置是把"ohos 线"声明在 ohosMain、"上游原版体"声明在 jvmMain(coroutines 1.10.2 / compose 1.10.3 / lifecycle 2.10.0,Maven Central),commonMain 保持线无关——jvm 侧换线不换版,交叉验证的说服力来自上游原体;ohos 侧取社区体则是制品可达性的唯一选择。

决策 4:派发期订阅代替后台 collector

uiEffects 是 replay=0 的 SharedFlow,效果只投递给"发射时已在订阅"的 collector。最初实现用 Dispatchers.Default 后台常驻 collector + 队列,结果撞上 synchronized 在 Kotlin/Native 不可用的问题。改成派发期订阅(UNDISPATCHED 内联订阅 → 派发 → yield 投递 → 取消):语义上精确复刻上游测试的模式,工程上消灭了全部共享可变状态与锁,还顺带避开了鸿蒙线 Dispatchers.Main 的跨线程派发问题。

决策 5:jvmTest 交叉验证 + 测试零改动

引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;25 项 jvmTest 跑的就是上游原测试文件(含 Koin 用例),是"内核没漂"的闸门。koin-core 没有 ohos 制品,处置不是改测试,而是"测试统一 JVM + 根构建禁用 Ohos*Test*"——测试文件一个字节不动,交叉验证的有效性才成立。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x86_64)构建宿主、JDK 21、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :simple-mvi-core:jvmTest :simple-mvi-compose:jvmTest

25 项用例全部通过(上游原测试文件,零改动):

测试类用例数钉住什么
MviConfigTest(core)11钩子先于 handleIntent 通知、configure 合并语义、reset、tryEmit 拒绝不通知 onEffect、抛异常钩子不破坏 store / 效果投递
MviStoreEffectsTest(core)5效果投递、replay=0、SUSPEND 缓冲拒绝、DROP_OLDEST 保最新
MviStoreKoinTest(core)7Koin 单例 / 工厂 / 多契约共存下的 store 隔离
MviViewModelConfigTest(compose)2ViewModel 路径的钩子顺序、sendEffect 拒绝回报

jvm 线依赖与上游逐字相同(换线不换版),上游测试照跑全绿——这正是"源码零改动、行为无漂移"的直接证据。

6.3 HAP 验证

:simple-mvi-core:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:simple-mvi-compose:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: SimpleMviDispatch / SimpleMviVersion 均为全局 T 符号
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

libohossimplemvi.so arm64 约 4.9MB、x86_64 约 4.4MB(debug,含 Kotlin/Native 运行时、coroutines、compose runtime 与 lifecycle klib);每个 ABI 三件套(libsimplemvi_napi.so / libohossimplemvi.so / libc++_shared.so)由 CMake $ORIGIN rpath 保证同目录互相可见。

6.4 功能用例

  1. 启动即握手:标题下版本行出现 0.4.0-ohos.1(来自 SimpleMviVersion() 真实 Native 调用),会话页自动建会话,状态卡 count=0;
  2. target 切到 MviViewModel · compose 连点 +1:count 与 operations 同步增长——compose 模块的 ViewModel 路径在真机执行;
  3. 切回 core,+N 把 count 推过 10:状态卡 count 变主色,效果列表出现 ThresholdReached(threshold=10);
  4. 点 重置:count 归 0,效果列表追加 ResetPerformed;
  5. Announce 发送任意文本:效果列表出现 Announced(text=…)(挂起 emitEffect 通道);
  6. 两个 target 交替发同一串 intent:状态迁移一致——同一逻辑、两条编译路径;
  7. 观测页签:钩子记录按 intent / effect / error 分色滚动,读取并清空生效;
  8. 安装抛异常钩子后再发 intent:状态照常更新,观测记录出现 error: hook exploded (by request)——上游"观测永不破坏 store"契约在真机成立;
  9. 重置 MviConfig 后再发 intent:观测记录不再增长(恢复 no-op);
  10. 杀进程重启:会话重建(count 归零)——桥会话是内存态,与上游"状态在 StateFlow、持久化是宿主的事"的边界一致。

6.5 验证结论

状态引擎、coroutines 运行时、compose 模块的 ViewModel 包装、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的 MVI 会话:版本握手、双 target 意图分发、阈值效果、挂起发射、全局观测与异常钩子契约(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/SimpleMVI.git
cd SimpleMVI
export JAVA_HOME="/path/to/jdk-21"    # Windows: $env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
./gradlew :simple-mvi-core:compileKotlinOhosArm64 :simple-mvi-compose:compileKotlinOhosArm64   # klib
./gradlew :simple-mvi-core:jvmTest :simple-mvi-compose:jvmTest                                # 25 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
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.simplemvi -m entry
hdc shell hilog | grep SimpleMvi

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :simple-mvi-core / :simple-mvi-compose 真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. eazytec Nexus 代理是懒加载,首次取制品可能超过默认 30s 超时——gradle.properties 里钉 socketTimeout=180000 / connectionTimeout=180000,冷缓存失败先重试再排查;
  4. Windows 上 gradlew.bat 启动读的是 JAVA_HOME 环境变量,gradle.properties 的 org.gradle.java.home 在 wrapper 起来之后才生效——两个都要设;
  5. synchronized 在 Kotlin/Native 不可用(Unresolved reference 'synchronized'):后台常驻 collector 的方案随之作废,改为"所有入口从 ArkTS 单线程调用 + runBlocking 在调用线程驱动协程"的纪律,共享可变状态归零(kotlin-channel-event-bus 桥同款);
  6. compose ohos 线不发布 *-desktop 子制品,jvm 目标从该线解析必失败——依赖必须按源集分线(ohosMain 社区体 / jvmMain 上游原版体);
  7. ArkTS 严格模式会拒绝 Kotlin 习惯写法:@State 变量不能叫 tabIndex(撞组件框架保留通用属性名)、字符串没有 .isEmpty()(用 .length === 0)、JSON.parse 结果要 as 到明确接口类型(不能落在 object 上);
  8. build-profile.json5 的 signingConfigs 条目缺 material 字段会被 hvigor schema 拒绝——仓库内留空数组且 product 引用默认签名名,勾选自动签名后由 IDE 回填完整配置;
  9. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  10. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  11. ArkTS 不能直接 import Kotlin/Native 的 libohossimplemvi.so——HarmonyOS CAdapter 不导出业务 @CName 符号,必须经 CMake N-API 包装层;
  12. replay=0 的效果流要求订阅先于发射:派发期 UNDISPATCHED 订阅是语义保真的关键,"先发射再补订阅"会静默丢效果。

8.2 已知问题

  • OHOS 尚无 Compose 宿主:CollectUiEffects 可编译(klib)但不能在鸿蒙渲染,验收页由 ArkUI 承担;MviViewModel 以无 ViewModelStore 的方式实例化(对桥会话足够,onCleared 生命周期属宿主职责);
  • 效果流不跨 C ABI:桥按派发现场订阅(保住 replay=0 语义),不做 N-API 线程安全回表式推送;
  • ohos 线版本主体与上游不同(coroutines 1.10.2→1.11.0、compose 1.10.3→1.12.0、lifecycle 2.10.0→2.11.0):上游版本体没有 ohos 构建,jvm 交叉验证线保持上游原版体作为行为一致的闸门;
  • simple-mvi-android(SavedStateHandle)与两个 samples 留盘不进构建;发布、CI 发布线剥离,本 fork 不发 Maven Central;
  • 桥的请求解析仅扁平 JSON(MiniJson 纪律),嵌套请求按设计拒绝;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.simplemvi 匹配的签名。

8.3 未来优化方向

  • 增加 N-API 线程安全回表的效果推送(替代派发期订阅),让宿主可以持续订阅;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查(llvm-nm)和 HAP 构建;
  • 补充模拟器(x86_64)与其他设备 ABI 的验收矩阵和 uitest 自动化截图;
  • 上游 0.5.x 的 API 变化按同一模式跟进(*.upstream 逐文件对照即可)。

九、总结

9.1 核心难点回顾

上游 simple-mvi-core + simple-mvi-compose(全部 commonMain)
    → 验证 ohos 制品线(coroutines / compose / lifecycle 三件套)
    → 工具链对齐 Kotlin 2.4.20-0.1.0-07(Gradle 9.6.1)
    → 根构建替换(cmp_qrose 模式,依赖按源集分线)
    → ohosArm64 / ohosX64 KLIB,零新增 Kotlin 源码
    → C ABI(@CName × 2)+ 扁平 JSON + 双 target 会话
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机三页签验收(意图 / 效果 / 全局观测 / 异常钩子契约)

9.2 封装层次

  • 引擎层:上游原版 MviStore / createStore / BaseMviStore,原子 updateState、replay=0 效果流、DROP_OLDEST 缓冲;
  • 观测层:上游原版 MviConfig,进程级钩子、合并语义、"钩子异常绝不破坏 store"的异常隔离;
  • compose 层:上游原版 MviViewModel,无 UI 实例化驱动,与 core target 共享同一份意图处理;
  • Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、JSON 解析、意图分发、效果与观测日志渲染;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先验证生态制品线再定适配路线:lifecycle ohos 线的补齐让"隔离 Compose"成为历史——SimpleMVI 这类"状态容器 + ViewModel 包装"库的正确答案是双模块真编译、零新增源码;路线选择必须基于当天的制品可达性,而不是历史结论;
  2. 工具链版本跟着依赖走:Kotlin 钉版由 coroutines 制品线的 stdlib 元数据反推(2.4.20-0.1.0-07),Gradle 经验按工具链线区分(2.4.20 线升 9.x,2.2.21 旧线钉 8.14.x);
  3. 并发纪律比并发原语重要:Native 桥撞上 synchronized 不可用后,正确解法不是找替代锁,而是重新设计成"单线程入口 + runBlocking 驱动 + 派发期订阅",让共享可变状态消失——语义(replay=0)反而比后台 collector 方案更保真。

9.4 适配成果

当前适配已完成:

  • SimpleMVI 0.4.0(0.4.0-ohos.1)core 与 compose 双模块的双 ABI Kotlin/Native 编译(上游 commonMain 文件零重写、零隔离,ohos 侧零新增 Kotlin 源码);
  • HarmonyOS 制品线在状态容器库上的完整落地(coroutines 1.11.0-0.1.0-07 + compose 1.12.0-0.1.0-07 + lifecycle 2.11.0-0.1.0-07);
  • 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 三页签验收页面(双 target 意图分发、阈值效果、挂起发射、全局观测、异常钩子契约、版本握手、操作日志);
  • 25 项 JVM 交叉验证全绿(上游原测试文件)+ HAP 构建(双 ABI 六件套 so)、签名、设备安装和真机验证;
  • AtomGit 项目文档、OpenHarmony README(中英双份)、上游构建备份(*.upstream)和本地效果图。

适配过程 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐