本文记录 compose-material3-navigation 接入 OpenHarmony 的完整过程,覆盖导航器语义盘点、鸿蒙 Kotlin 工具链钉版、纯 Kotlin 引擎提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

compose-material3-navigation 是一个 Compose Multiplatform 库:把 Material3 的 ModalBottomSheet 变成 Navigation Compose 的导航目的地——bottomSheet(route) { ... } 注册目的地,rememberModalBottomSheetNavigator() + ModalBottomSheetLayout 负责把每次导航渲染成一个可拖拽、可深链、有独立生命周期的模态底部弹窗。上游依赖 androidx.navigation(Navigator / NavDestination / NavBackStackEntry)与 androidx.compose.material3,这两条制品线在 ohos 上都不存在,因此本次适配与社区早期"零依赖内核直接编译"的 qrose 路线同源但更进一步:把上游 Navigator 赖以工作的状态机(NavigatorState.push / popWithTransition / markTransitionComplete、entry 生命周期、深链匹配)提取为零依赖纯 Kotlin 引擎,编进 ohos 目标,Compose 绑定文件原样隔离留档,ArkUI 侧复刻 ModalBottomSheetLayout / ModalBottomSheetHost 的宿主行为。这与社区已过审的 cmp_qrose / navigation-compose-extended / compose-navigation-reimagined 是同一条路线。

项目地址: AtomGit/oh-tpc/compose-material3-navigation

上游仓库: eygraber/compose-material3-navigation

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

上游库解决一件事:在 Navigation Compose 里让底部弹窗成为一等公民的导航目的地。三个 API 撑起全部能力:rememberModalBottomSheetNavigator() 创建挂进 rememberNavController(...) 的 Navigator<Destination>;NavGraphBuilder.bottomSheet(route) { entry -> ... } 把一段 @Composable 注册成 FloatingWindow 目的地;ModalBottomSheetLayout(navigator) { NavHost(...) } 负责把 backstack 里可见的 entry 渲染成 Material3 模态底部弹窗(带 scrim、拖拽、生命周期、深链)。

它值得移植的原因:底部弹窗导航的语义层——返回栈、转场跟踪、生命周期驱动、saveState/restoreState 簿记、深链匹配——在任何一个移动平台上都是通用资产,这一层是"值得编译保留的东西";而 UI 层(弹窗动画、手势物理学)与 androidx.compose.material3 强耦合,在鸿蒙上由 ArkUI 等价宿主承担。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享;OpenHarmony 应用同样需要"弹窗也是导航状态"的跨平台导航模型,但上游构建(Kotlin 2.2.21 / Gradle 9.2.1 / com.eygraber.conventions* 插件链)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明导航器状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证状态机引擎、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
依赖矩阵上游 api(org.jetbrains.androidx.navigation:navigation-compose:2.9.1) 与 compose.material3 均无 ohos 变体,依赖解析期直接失败——整库直编这条路在制品层面就不通。
工具链不一致上游根构建挂在 com.eygraber.conventions* 约定插件上(拉整条 AGP / JetBrains Compose 工具链),与鸿蒙工具链无法共存于一个 Gradle 构建;旧钉法 Kotlin 2.2.21-1.0.0 只能配 Gradle 8.14.x。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
语义跨不了桥Composable 参数(content / dragHandle / confirmValueChange / contentWindowInsets)是函数,过不了 FFI;StateFlow 响应式载体需要 coroutines 制品,ohos 上没有。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:导航器状态机的纯 Kotlin 引擎提取与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。弹窗的渲染编排(scrim、拖拽、锚点动画)完全由 ArkUI 宿主承担,backstack 真相永远以引擎响应为准。

1.2 库提供的能力

Demo 提供一个真实的底部弹窗导航会话,对应桥接的全部入口:

  • 导航 push:navigate(details / locked / share) 把 entry 压入引擎 backstack,页面按 rememberVisibleList 语义只展开达到 STARTED 的栈顶 entry;
  • Android 转场语义:pushWithTransition 复刻 Android actual 的 state.pushWithTransition(entry 直接进 transitionsInProgress);
  • dismiss / pop / popUpTo:dismiss 即上游 onDismissRequest → navigator.dismiss(entry) → popWithTransition;popUpTo(home, saveState) 弹到栈底方向全部出栈并批量进转场,saveState 记入 savedStateRoutes;
  • 生命周期驱动:op=lifecycle 由宿主按 CREATED → STARTED → RESUMED 推进,对应 NavBackStackEntry 生命周期;
  • 转场排空:隐藏动画结束补发 onTransitionComplete,对应 state.markTransitionComplete;
  • 深链匹配:demo://details/42?extra=hello 按 {placeholder} 路径段 + query 参数命中目的地后走 navigate 语义;
  • 版本握手:m3NavVersion() 返回 0.0.8-ohos.1,来自真实 Native 调用而非静态文字。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文。
op / version回显本次操作与 fork 版本。
engine全量状态:attached、destinations(含 spec 与 properties)、backStack(id/route/lifecycle/visible/arguments)、transitionsInProgress、savedStateRoutes。
log滚动操作日志(上限 240 条,响应带最近 60 条)。

引擎侧完整能力(attach 契约、push 两种语义、popUpTo 连带弹出、saveState 簿记与消费、STARTED 可见过滤、深链匹配、监听器通知)全部在引擎里实现,JVM 侧 25 项 jvmTest 交叉验证逐条钉住,证明 ohos .so 里执行的导航语义没有漂移。

1.3 实现适配

维度要求
代码复用上游 6 个 Compose 文件 git mv 进 composeMain 逐字节保留(等工具链对齐后接回);导航器状态机提取为 5 个纯 Kotlin 引擎文件,不重写语义只搬语义;UI 编排按 ArkUI 等价复刻并逐项标注上游出处。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);sample/*、androidMain、cmpMain、kotlin-js-store 留盘不进 settings.gradle.kts。
桥接稳定单一 C ABI 入口 M3NavDispatch(op 分发)+ UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律引擎零依赖纯 Kotlin(listener 回调代替 StateFlow),坚持 cmp_qrose 已过审的零依赖纪律。
UI 完整四 Tab 覆盖全部桥 op:导航(backstack 可视化)、Sheet 行为(spec 与 properties)、深链、关于(接口对照与版本握手)。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。上游的 Compose 绑定层不参与鸿蒙编译,ArkTS Demo 是 ModalBottomSheetLayout + ModalBottomSheetHost 这对宿主的等价复刻:backstack 状态来自 Kotlin/Native 引擎,页面只喂手势、播锚点动画、画帧。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与依赖边界、确认 androidx 制品缺口、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:桥协议建模     ── 引擎状态 JSON 契约、12 个 op、错误通道与滚动日志
第 4 阶段:原生桥接       ── @CName C ABI、C++ N-API、生成头回填、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── 宿主行为复刻、生命周期驱动、锚点判定、四 Tab Demo
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
compose-material3-navigation/          上游库模块
library/src/composeMain/               上游 6 个 Compose 绑定文件(git mv 保留,不参与编译)
library/src/commonMain/engine/         新增:纯 Kotlin 引擎(5 个文件,编 jvm + 双 ohos)
library/src/commonTest/engine/         新增:25 项语义测试(JVM 交叉验证)
library/src/androidMain/ 等            上游平台 actual,留盘不进构建
example/nativeApp/                     Kotlin/Native 动态库(Bridge + Json)
example/harmonyApp/                    DevEco Stage、CMake N-API 和 ArkUI 页面
sample/                                上游示例,留盘不进 settings
README.OpenHarmony.md / _CN.md         鸿蒙适配说明(中英双份)
README.upstream.md                     上游原文保存(git mv 保留)

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :library 与 :example:nativeApp,rootProject.name = "ohos_compose-material3-navigation"。上游的 com.eygraber.conventions* 约定插件、AGP classpath、vanniktech 发布插件、develocity 构建扫描全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven)
JDK21(Temurin jdk-21.0.12.101-hotspot,org.gradle.java.home 固定写入)
OpenHarmony 目标ohosArm64 + ohosX64(jvm() 保留作交叉验证)
Gradle8.14.1(wrapper 从上游 9.2.1 降级,2.2.21 旧线纪律)
DevEco product5.1.0(18) 兼容
DevEco Studio26.0.0.851 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.m3navigation
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
上游文件(commonMain)依赖形态处置
ModalBottomSheetNavigator.ktexpect class + NavigatorState + Material3 类型语义下沉引擎;原文件 git mv → composeMain
ModalBottomSheetLayout.ktCompose 宿主入口git mv → composeMain;ArkTS Stack 布局承接
ModalBottomSheetHost.kt可见列表过滤、BackHandler、转场回调git mv → composeMain;ArkTS syncSheetFromEngine() 承接
ModalBottomSheetNavGraphBuilder.ktbottomSheet() DSLgit mv → composeMain;桥 op=register 承接
ModalBottomSheetNavigatorDestinationBuilder.ktNavDestinationBuilder 子类git mv → composeMain;SheetDestinationSpec 承接
BackHandler.ktinternal expect fungit mv → composeMain;宿主返回键策略承接

上游源码全部依赖 androidx.navigation 与 androidx.compose.*,这两条制品线在 ohos 上不存在,所以 6 个文件一个都不能编——它们整体 git mv 进 composeMain(git 识别为 R100,历史完整保留),作为对照上游 API 的留档。引擎侧新增 5 个纯 Kotlin 文件(com.eygraber.compose.material3.navigation.engine 包):SheetEntryLifecycleState(镜像 Lifecycle.State)、SheetPropertiesSpec(ModalBottomSheetProperties 的 4 个布尔纯数据化)、SheetDestinationSpec(Destination 的可跨界字段)、SheetEntry(backstack entry,自增 id 身份)、ModalBottomSheetEngine(状态机本体)。

剥除注解:上游 android actual 的 @Navigator.Name(...)、@NavDestination.ClassType(...) 是 JVM 注解,引擎不携带;@ExperimentalMaterial3Api 只在 composeMain 里保留。

1.4 页面边界
Tab 1  导航          navigate(details/locked/share)、pushWithTransition、pop()/dismiss(top)/popUpTo(home)
                    saveState 开关、backStack 可视化(id/route/lifecycle/visible/arguments)、
                    transitionsInProgress、引擎操作日志
Tab 2  Sheet 行为    每个目的地的 spec 卡片(skipPartiallyExpanded、hasDragHandle、properties 四布尔、deepLinks)、
                    模拟返回键(shouldDismissOnBackPress 策略判定)
Tab 3  深链          URI 输入 + 预设按钮,命中后走 navigate 语义
Tab 4  关于          版本握手、上游 API ↔ 桥 op 全对照、仓库与社区链接
弹窗层              scrim(点击 dismiss)+ 栈顶可见 sheet 面板(拖拽条/锚点/参数展示/关闭)

页面启动时 aboutToAppear 先调 m3NavVersion() 做版本握手,再按 bottomSheet DSL 语义注册三个目的地并 attach(等价 NavController setGraph)。

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

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

上游 library/build.gradle.kts 的依赖表只有四行,逐个对 ohos 制品可达性验证:

org.jetbrains.androidx.navigation:navigation-compose:2.9.1
  Gradle .module 元数据:变体仅 android / ios / jvm / js / wasmJs —— 无 ohosArm64 / ohosX64
org.jetbrains.compose:*,androidx.compose.material3(CMP 1.9.3)
  社区 CMP 1.9.x 线没有 composition 宿主;上游 1.12/Kotlin 2.4 与社区工具链互斥
org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.2
  官方 Central 无 ohos 变体

结论:androidx.navigation 是上游的 api 依赖,状态机的真正载体 NavigatorState(push / popWithTransition / markTransitionComplete)在这个包里——它没有 ohos 制品,整库直编在依赖解析期就会失败,提取状态机是唯一可行路线。这也解释了为什么引擎连 StateFlow 都不能依赖:coroutines 无 ohos 制品,桥是同步一来一回,响应式载体换成 listener 回调。

2.2 工具链钉版(为什么是 2.2.21-1.0.0 + Gradle 8.14.1)

沿用社区已过审的钉法:HarmonyOS Kotlin 2.2.21-1.0.0(qrose / navigation-compose-extended / compose-navigation-reimagined 同款)。这条线必须配 Gradle 8.14.x,上游 wrapper 是 9.2.1,直接降级:

distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.1-bin.zip

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()
    }
}

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.PREFER_PROJECT)
    repositories {
        maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
        maven("https://maven.eazytec-cloud.com/nexus/content/groups/harmonyos")
        google()
        mavenCentral()
    }
}

rootProject.name = "ohos_compose-material3-navigation"

include(":library")
include(":example:nativeApp")

eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64。根构建替换为极简 ohos 构建:

plugins {
    alias(libs.plugins.kotlin.multiplatform) apply false
}

allprojects {
    group = "com.eygraber"
    version = providers.gradleProperty("version").orElse("0.0.8-ohos.1").get()
}

subprojects {
    // The HarmonyOS Kotlin toolchain has no test runner for ohos targets;
    // engine semantics are cross-validated with commonTest on the JVM target.
    tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
        .configureEach { enabled = false }
}

gradle.properties 同步收敛:JVM 参数降为 -Xmx2g、写入 org.gradle.java.home(JDK 21)、保留 kotlin.native.ignoreDisabledTargets=true / kotlin.native.distribution.downloadFromMaven=true,上游的 Android/Compose/发布属性全部剥离。

2.3 编译真实引擎

library/build.gradle.kts 收敛为纯 KMP 模块:

plugins {
    alias(libs.plugins.kotlin.multiplatform)
}

kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()
    ohosArm64()
    ohosX64()

    sourceSets {
        // The engine extracted for OpenHarmony is dependency-free plain Kotlin.
        commonMain.dependencies {}

        commonTest.dependencies {
            implementation(kotlin("test"))
        }
    }
}

引擎状态机与上游 NavigatorState 的逐点对照(每条都有对应测试):

上游引擎对应语义钉子(jvmTest)
isAttached 之前 backStack 读作空列表attach() 前 backStack 返回空,导航抛 IllegalStateExceptionbackStackReadsEmptyBeforeAttach
navigate(entries) → state.push(CMP actual)navigate(route, args, withTransition=false)cmpPushDoesNotEnterTransition
Android actual 的 state.pushWithTransitionnavigate(..., withTransition=true)androidPushWithTransitionEntersTransition
dismiss(entry) → popBackStack(entry, false) → popWithTransitiondismiss(entry):出栈 + 进 transitionsInProgressdismissRemovesFromBackStackAndMarksTransition
state.markTransitionComplete(entry)onTransitionComplete(entry)onTransitionCompleteDrainsTransition
popBackStack(popUpTo, savedState) 连 popUpTo 一起弹弹到栈底方向全部出栈并批量转场popBackStackPopsUpToAndIncluding
NavOptions.saveState/restoreStatesavedStateRoutes 簿记,navigate 消费saveStateIsConsumedByLaterNavigate
rememberVisibleList:Lifecycle ≥ STARTED 过滤visibleEntries()visibleEntriesFilterNonStartedEntries
NavController 深链匹配deepLink(uri):{placeholder} 路径段 + query3 项深链测试

实测:

./gradlew :library:compileKotlinJvm :library:jvmTest   # 25 项全绿
./gradlew :example:nativeApp:compileKotlinOhosArm64     # BUILD SUCCESSFUL
2.4 测试的处置:ohos 没有测试 runner

根构建全局禁用 *Ohos*Test* 任务(见 2.2),测试统一在 JVM 跑。引擎源集是 commonMain,JVM 与 ohos 跑同一份代码——25 项测试跑的就是将要进 .so 的那份逻辑,这是"内核没漂"的证据链。测试是在提取引擎时同步编写的(上游没有引擎测试可直接搬,上游语义锚在 NavigatorState / ModalBottomSheetHost 的行为里),每个测试对应一条可指认的上游行为。

第 3 阶段:桥协议建模

3.1 JSON 契约
com.eygraber.compose.material3.navigation.engine/**(纯 Kotlin 引擎,原样编译)
        ↓ backstack / transitions / visible / lifecycle / 深链
Bridge.kt(@CName × 2,单入口 op 分发)
        ↓ UTF-8 JSON
N-API m3NavDispatch(request) / m3NavVersion()
        ↓
ArkUI Index.ets(四 Tab + 弹窗层)

请求与响应:

请求 : {"op":"navigate","route":"details","arguments":{"itemId":"42"},"withTransition":false}
       {"op":"register","route":"locked","skipPartiallyExpanded":true,"hasDragHandle":false,
        "properties":{"shouldDismissOnBackPress":false,"shouldSecureCanvas":true},"deepLinks":[]}
       {"op":"dismiss","entryId":7} / {"op":"popUpTo","entryId":1,"saveState":true}
       {"op":"onTransitionComplete","entryId":7}
       {"op":"lifecycle","entryId":7,"event":"RESUMED"}
       {"op":"deepLink","uri":"demo://details/42?extra=hello"}
响应 : {"ok":true,"op":"...","version":"0.0.8-ohos.1","engine":{...},"log":[...]}
       {"ok":false,"error":"..."}

12 个 op 与上游 API 的对应:version(握手)、attach/detach(setGraph/teardown)、register(bottomSheet() DSL)、navigate(NavController.navigate → state.push)、dismiss(onDismissRequest → navigator.dismiss)、popUpTo(popBackStack(entry, saveState))、pop(栈顶 dismiss 便利封装)、onTransitionComplete(markTransitionComplete)、lifecycle(entry 生命周期驱动)、deepLink(深链匹配)、reset(清引擎与日志)、state(只读快照)。

3.2 为什么是单入口 op 分发

导航 ops 有 12 个,12 个 @CName 导出会让 NAPI 层的包装代码翻倍;单入口({"op":"..."})便于统一日志、统一错误信封、统一版本回显,与 compose-navigation-reimagined 的已过审桥形态一致。参数值约定为标量(字符串/数字/布尔),SheetEntry.arguments 为 Map<String, String>——路由参数场景足够,NamedNavArgument 的 DSL 校验不纳入引擎。

3.3 错误通道

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

@CName("M3NavDispatch")
fun m3NavDispatch(request: String): String = try {
    val json = Json.parse(request)
    dispatch(json.str("op"), json)
} catch (t: Throwable) {
    JsonWriter()
        .beginObject()
        .field("ok", false)
        .field("error", t.message ?: t.toString())
        .endObject()
        .toString()
}

非法 JSON、缺 op / route / entryId、未知 route、空栈 pop、深链未命中,全部走同一个 ok:false 通道,ArkTS 侧统一渲染进状态行。navigate 对未知 route 的报错文案刻意对齐上游 NavController 的措辞(“Navigation destination that matches route … cannot be found”),错误信息本身也是语义保真的一部分。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libm3nav_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosm3nav.so)
  │ M3NavDispatch / M3NavVersion
  ▼
UTF-8 JSON + DisposeString

直接导出 Kotlin 对象会暴露 ABI 和生命周期问题;只导出整数又会让页面复制导航语义,所以选择 C ABI + JSON。ArkTS 不能直接 import libohosm3nav.so——HarmonyOS 的 CAdapter 不会把 @CName 业务符号挂到 ArkTS,NAPI 包装层才是 ArkTS 的合法入口。

4.2 Kotlin/Native 导出函数
@CName("M3NavDispatch")
fun m3NavDispatch(request: String): String   // 12 个 op 一口进

@CName("M3NavVersion")
fun m3NavVersion(): String = VERSION          // "0.0.8-ohos.1" 版本握手
C ABI 符号ArkTS 方法覆盖内核
M3NavDispatchm3NavDispatch(req)引擎注册表 + backstack + 转场 + 生命周期 + 深链
M3NavVersionm3NavVersion()fork 版本握手(真机版本行的数据来源)
4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"m3NavDispatch", nullptr, NapiM3NavDispatch, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"m3NavVersion", nullptr, NapiM3NavVersion, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosm3nav_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free():

static void DisposeKnString(const char *raw) {
    if (raw == nullptr) { return; }
    libohosm3nav_ExportedSymbols *symbols = libohosm3nav_symbols();
    if (symbols != nullptr && symbols->DisposeString != nullptr) {
        symbols->DisposeString(raw);
    }
}
4.4 CMake、生成头和类型声明
add_library(ohosm3nav SHARED IMPORTED)
set_target_properties(ohosm3nav PROPERTIES
  IMPORTED_LOCATION ${KN_SO}
  IMPORTED_NO_SONAME TRUE)

add_library(m3nav_napi SHARED napi_init.cpp)
target_link_libraries(m3nav_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosm3nav)

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

一个与社区早期仓库不同的点:libohosm3nav_api.h 不是手写的,而是 Kotlin/Native 链接时自动生成的头(声明 ExportedSymbols 结构与全部业务符号),由 nativeApp 的 copyKnHeader 任务挂 finalizedBy 在 link 后自动拷进 entry/src/main/cpp/include/——生成头与 .so 永远同源,不存在手抄漂移。

export const m3NavDispatch: (request: string) => string;
export const m3NavVersion: () => string;

第 5 阶段:ArkUI 宿主

5.1 注入引导和触发操作
aboutToAppear(): void {
  this.version = m3NavVersion();   // 版本握手:来自真实 Native 调用
  this.bootstrap();                // reset → register × 3 → attach
}

dispatch(request: DispatchRequest): void {
  const raw: string = m3NavDispatch(JSON.stringify(request));
  const response: DispatchResponse = JSON.parse(raw) as DispatchResponse;
  if (!response.ok) { /* 错误原文进状态行 */ }
  if (response.engine) { this.engine = response.engine; }   // backstack 真相整体刷新
  if (response.log) { this.logLines = response.log; }
  this.syncSheetFromEngine();      // rememberVisibleList 等价物
}

页面不做本地推算:每次操作后用响应里的 engine 字段整体刷新状态,锚点动画只决定"怎么画",不决定"栈里有什么"。

5.2 宿主行为复刻点
上游宿主行为(ModalBottomSheetLayout / Host)ArkTS 对应
content() + Host 叠层Stack { Tabs(宿主内容); scrim; SheetPanel }
rememberVisibleList 只组 STARTED entrysyncSheetFromEngine() 只组 visible=true 的栈顶 entry
push 后 CREATED,组合后 STARTED,展开后 RESUMEDnavigate 成功 → op=lifecycle started → 展开动画后 → resumed
ModalBottomSheet(onDismissRequest = { dismiss(entry) })scrim 点击 / 拖拽越阈值 → op=dismiss
sheet 锚点(half/full)与 skipPartiallyExpanded本页锚点状态机:skip 目的地只有 full/hidden 两个锚
dragHandle ?: nullRect 拖拽条 vs “dragHandle = null” 徽标
shouldDismissOnBackPress 才挂 BackHandler"模拟返回键"按钮:false 时直接拒绝并提示
onDispose → onTransitionComplete隐藏动画结束 → op=onTransitionComplete

拖拽落位判定(settleSheet)按 M3 SheetState 规则在本页实现:拖过阈值或快速下甩触发 hide(dismiss),否则回弹最近锚点;skipPartiallyExpanded 的目的地在半展开处被否决,只能 full/hidden 二选一。

5.3 对比度和错误边界

操作按钮语义配色:导航蓝、Android 转场金棕、破坏性操作红;ok:false 时错误原文进状态行;返回键策略拒绝时明确提示 “BackHandler 被拒: shouldDismissOnBackPress=false (locked 目的地)”。Native 调用本身抛错(理论不该发生)同样被 try/catch 收进状态行,页面永不因桥异常白屏。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── nativeApp/
│   ├── build.gradle.kts                      双 ABI sharedLib(baseName=ohosm3nav)+ cxxShared + copyKnSo*/copyKnHeader
│   └── src/ohosMain/kotlin/org/terminator/ohos/m3nav/
│       ├── Bridge.kt                         @CName × 2 + op 分发 + 状态序列化(276 行)
│       └── Json.kt                           手写递归下降解析器 + JsonWriter(243 行)
└── harmonyApp/
    ├── build-profile.json5                   signingConfigs 留空(签名由 DevEco 自动生成回填)
    ├── AppScope/app.json5                    bundleName org.terminator.ohos.m3navigation、versionName 0.0.8-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/libohosm3nav_api.h   KN 生成头,copyKnHeader 自动回填
        ├── src/main/cpp/types/m3nav_napi/{index.d.ts, oh-package.json5}
        └── src/main/ets/pages/Index.ets      ArkUI 验收页面(649 行,四 Tab + 弹窗层)
6.2 构建、签名和安装
./gradlew :library:jvmTest                                        # 25 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64

cd example/harmonyApp
hvigorw assembleHap -p product=default --no-daemon

签名在 DevEco Studio 的 File → Project Structure → Signing Configs 中勾选自动生成,证书写入本机,不进入仓库(仓库内 build-profile.json5 的 signingConfigs 保持空数组)。

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

四、完整代码对照

4.1 整体架构

library/src/composeMain/**(上游 Compose 绑定,逐字节保留留档)
    │ 等社区 CMP 工具链对齐后接回,当前不参与编译
library/src/commonMain/engine/**(纯 Kotlin 引擎)
    │ ModalBottomSheetEngine / SheetEntry / SheetDestinationSpec
    │ SheetPropertiesSpec / SheetEntryLifecycleState
Bridge.kt + Json.kt
    │ M3NavDispatch / M3NavVersion
libm3nav_napi.so
    │ m3NavDispatch(req) / m3NavVersion()
Index.ets
    │ JSON parse + @State 更新 + 宿主编排
    ▼
ArkUI 真机页面(四 Tab + scrim + sheet 面板)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_compose-material3-navigation、sample 与上游平台源集移出
gradle/libs.versions.toml只剩 kotlin-multiplatform 2.2.21-1.0.0 一个插件
build.gradle.kts(根)版本 0.0.8-ohos.1、全局禁用 Ohos*Test* 任务
gradle/wrapper/gradle-wrapper.propertiesGradle 9.2.1 → 8.14.1
library/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,去 conventions 插件链
library/src/commonMain/engine/**(5 文件)状态机本体,416 行,零依赖
library/src/commonTest/engine/**25 项语义测试,299 行
example/nativeApp/build.gradle.kts双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* + copyKnHeader 回填
nativeApp/.../Bridge.kt2 个 C ABI、op 分发、状态序列化、滚动日志、错误通道
nativeApp/.../Json.kt手写递归下降 JSON 解析 + JsonWriter(零依赖)
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鸿蒙适配主文档(23 节)

4.3 关键 API 对照

层次API作用
上游rememberModalBottomSheetNavigator()创建导航器(op=attach 对应其挂图时机)
上游NavGraphBuilder.bottomSheet(route, ...)注册目的地(op=register)
上游NavController.navigate → state.push压栈(op=navigate)
上游navigator.dismiss / popBackStack出栈并进转场(op=dismiss / pop / popUpTo)
上游state.markTransitionComplete转场排空(op=onTransitionComplete)
上游rememberVisibleList(STARTED 过滤)engine.visibleEntries()(entry.visible)
上游ModalBottomSheetPropertiesSheetPropertiesSpec 四布尔(op=register 载荷)
NativeM3NavDispatch / M3NavVersionC ABI JSON 导出
N-APIm3NavDispatch(req) / m3NavVersion()向 ArkTS 暴露能力
ArkTSdispatch(op)触发操作并整体刷新 UI

4.4 ArkTS 与 Kotlin 的边界

// 注册目的地(等价 bottomSheet DSL)+ 导航 + 全量状态
m3NavDispatch(JSON.stringify({
  op: 'register', route: 'cart', skipPartiallyExpanded: false, hasDragHandle: true,
  deepLinks: ['app://cart/{itemId}'],
  properties: { shouldDismissOnBackPress: true, isAppearanceLightStatusBars: false,
    isAppearanceLightNavigationBars: false, shouldSecureCanvas: false }
}));
m3NavDispatch(JSON.stringify({ op: 'attach' }));
const raw = m3NavDispatch(JSON.stringify({ op: 'navigate', route: 'cart', arguments: { itemId: '42' } }));
const state = JSON.parse(raw) as DispatchResponse;   // ok/op/version/engine/log
// 引擎侧:纯 Kotlin 状态机,宿主任意驱动,不依赖任何 UI 框架
val engine = ModalBottomSheetEngine()
engine.registerDestination(SheetDestinationSpec(route = "cart"))
engine.attach()
val entry = engine.navigate("cart", arguments = mapOf("itemId" to "42"))
engine.setEntryLifecycleState(entry.id, SheetEntryLifecycleState.STARTED)  // 可见
engine.dismiss(entry)                                                      // 出栈 + 转场
engine.onTransitionComplete(entry)                                         // 转场排空

两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构;lifecycle 与 onTransitionComplete 是宿主的两个纪律性义务——组成了发 started、展开了发 resumed、隐藏动画结束必须补发转场完成,否则 transitionsInProgress 不清空。

五、关键决策说明

决策 1:提取状态机而不是硬编 Compose,也不是纯 ArkTS 重写

androidx.navigation 没有 ohos 制品,整库直编在依赖解析期就失败;社区 CMP 1.9.x 线没有 composition 宿主,硬编等于自己维护一套 CMP 发行版;纯 ArkTS 重写则把状态机也重写一遍,跨平台共享层的价值归零。三条路都堵死后,唯一答案是把上游 Navigator 赖以工作的 NavigatorState 语义逐条镜像成零依赖纯 Kotlin——搬语义,不抄实现,每条语义配一个 JVM 测试钉住。

决策 2:工具链钉 2.2.21-1.0.0 + Gradle 8.14.1,不追 2.4.20 线

KStore 适配用 2.4.20-0.1.0-07,前提是它的依赖(coroutines 制品线)要 2.4 元数据;本项目引擎零依赖,没有版本压力,按已过审的 2.2.21-1.0.0 保守钉法走,代价是 Gradle 必须从上游 9.2.1 降到 8.14.1(2.2.21 旧线纪律)。两条工具链线的经验不能互相套用:有 ohos kotlinx 依赖的库走 2.4.20 线,零依赖引擎走 2.2.21 线。

决策 3:根构建替换(cmp_qrose 模式),上游模块留盘

上游根构建挂在 com.eygraber.conventions* 约定插件上(target 创建、Android 配置、发布都在插件里,本仓库没有源码),对鸿蒙没有保留价值,整体替换为极简 ohos 构建。sample/* 与上游平台 actual(androidMain/cmpMain)留盘不进 settings.gradle.kts,目录的存在方式只有这一种;composeMain 不注册进构建,但 git mv 保留历史,上游升级时 diff 即可比对。

决策 4:零依赖引擎 + listener 回调,不上 StateFlow

坚持 cmp_qrose 已过审的零依赖纪律(纯 Kotlin 编 ohos,无任何外部制品):coroutines 无 ohos 制品,StateFlow 无从解析;且桥是同步一来一回,Flow 没有落地场景。引擎用 addListener/removeListener 提供变更通知,JVM 测试验证触发次数,未来社区 coroutines 制品线补齐时可平滑换回 StateFlow 对齐上游签名。

决策 5:单 dispatch + op 分发 + 全量状态回显

12 个 op 收进一个 @CName 入口({"op":"..."}),NAPI 层只需包装两个符号;每个响应都携带全量 engine 状态与滚动日志,ArkTS 端以"操作后整体刷新"替代增量推送——没有增量同步,就没有状态漂移。

决策 6:25 项 JVM 测试是语义闸门

ohos 目标没有测试 runner,引擎语义的正确性只能靠交叉验证:25 项测试与引擎同步编写,每条对应一个可指认的上游行为(attach 契约、push 两种语义、dismiss=popWithTransition、popUpTo 连带弹出、saveState 消费、STARTED 过滤、深链三分支、监听器次数、边界拒绝)。JVM 与 ohos 跑的是 commonMain 同一份代码,测试全绿即"so 里的导航语义没漂"。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Temurin jdk-21.0.12.101-hotspot)、Gradle 8.14.1、DevEco Studio 26.0.0.851(bundled hvigorw / ohpm / SDK 5.1.0(18))与鸿蒙真机(arm64)。

6.2 JVM 交叉验证

./gradlew :library:jvmTest

25 项用例全部通过:

语义组用例(示例)钉住什么
名字与 attach 契约navigatorNameMatchesUpstream、backStackReadsEmptyBeforeAttach、detachReadsEmptyAndBlocksNavigationNAME="modalBottomSheet";attach 前 backStack 空读、导航拒绝
push 语义navigatePushesEntriesInOrder、cmpPushDoesNotEnterTransition、androidPushWithTransitionEntersTransition栈序、自增 id;CMP 无转场 vs Android 有转场
出栈与转场dismissRemovesFromBackStackAndMarksTransition、popBackStackPopsUpToAndIncluding、onTransitionCompleteDrainsTransition、popPopsTheTopEntrydismiss=popWithTransition;popUpTo 连带弹出;转场排空
saveStatesaveStateIsConsumedByLaterNavigate簿记 + navigate 消费
可见性与生命周期visibleEntriesFilterNonStartedEntriesSTARTED 过滤(rememberVisibleList 语义)
深链deepLinkMatchesPathPlaceholderAndQuery、deepLinkMatchesPlainPattern、deepLinkWithoutMatchThrows路径参数 + query;未命中报错
边界popOnEmptyBackStackReturnsFalse、navigateUnknownRouteThrows、destinationRouteMustNotBeEmpty 等与上游一致的失败面

6.3 HAP 验证

:library:jvmTest: BUILD SUCCESSFUL(25 项)
:example:nativeApp:linkDebugSharedOhosArm64 / OhosX64: BUILD SUCCESSFUL
llvm-nm -D: M3NavDispatch / M3NavVersion 均为全局 T 符号(双 ABI)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully(真机)

entry-default-unsigned.hap 约 5.6 MB,内含双 ABI 各三件套(libm3nav_napi.so / libohosm3nav.so / libc++_shared.so);KN so debugShared 未 strip 时 arm64 约 2.6 MB、x86_64 约 2.2 MB。

6.4 功能用例

  1. 启动后"关于"Tab 与导航 Tab 顶部出现 版本 0.0.8-ohos.1(来自真实 Native 调用,非静态文字);
  2. 引导序列在日志区滚动留痕:engine reset → registerDestination(details/locked/share) → attach();
  3. 点 navigate(details),sheet 以半展开锚点弹出,日志出现 navigate(details) -> id=N [push],backStack 列表实时渲染 entry 的 id/route/lifecycle/visible;
  4. 弹窗展开后 entry 生命周期推进为 RESUMED(宿主发的 op=lifecycle);
  5. 下拉越过阈值或点 scrim,sheet 隐藏,transitionsInProgress 出现该 entry id,隐藏动画结束补发转场完成后清空;
  6. 连续 push 多个目的地后点 popUpTo(home),栈顶方向全部 entry 批量出栈并进入 transitionsInProgress(真机会话中观察到 [4, 3, 2, 1, 5, 8] 的批量转场列表);
  7. 打开 locked 目的地:skip 直接全展开、无拖拽条、shouldSecureCanvas=true;点"模拟返回键"被策略拒绝并提示;
  8. 深链 Tab 发送 demo://details/42?extra=hello,backStack 出现带参数的 details entry。

6.5 验证结论

导航器状态机、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的导航会话:版本握手、目的地注册、push、转场批量跟踪与 backstack 可视化闭环(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/compose-material3-navigation.git
cd compose-material3-navigation
export JAVA_HOME="/path/to/jdk-21"
./gradlew :library:jvmTest                                                      # 25 项交叉验证
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.m3navigation -m entry
hdc shell hilog | grep M3Nav

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让 :library 真正编译成 ohosArm64 / ohosX64 动态库,且语义有测试钉住;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. 上游根构建挂在 com.eygraber.conventions* 约定插件上,根构建替换时整个插件链(AGP / Compose / 发布)一并剥离——gradle.properties 里残留的 Android/Compose 属性也要同步清掉;
  4. commonMain 里不能用 JVM 专属 API:putIfAbsent 是 JDK 8 Map 默认方法、kotlin.synchronized 是 JVM 专属,编译 ohos 时才报 Unresolved reference——改手写 containsKey 判断;锁直接删掉,ArkTS 单线程调用无需加锁;
  5. 表达式函数体(= synchronized { ... })改块体时容易丢 return,报 Missing return statement;
  6. rememberVisibleList 的语义是"STARTED 才可见"——测试断言"push 后立即可见"是错的,不是引擎 bug;push 后 entry 是 CREATED,必须由宿主推进生命周期;
  7. ArkTS(严格模式)不支持对象展开 { ...obj } 与 ! 非空断言,重建对象用显式字段拷贝;.flexWrap 是 Flex 容器属性,Row 上没有,编译期直接报 Property 'flexWrap' does not exist on type 'RowAttribute';
  8. NAPI 头(libohosm3nav_api.h)是 KN 链接产物:不要手抄,用 copyKnHeader 任务挂 finalizedBy 自动回填,生成头与 .so 永远同源;
  9. NAPI 层字符串纪律:napi_get_value_string_utf8 的缓冲区在 API 26 头文件里是非 const,走 std::vector 中转;KN 返回串必须 DisposeString,不能 free();
  10. hvigorw assembleHap 的打包步(PackageHap)要 spawn java,命令行构建时必须把 DevEco 自带 JBR 加进 PATH,否则报 spawn java ENOENT(错误码 00308018);
  11. HAP 缺当前 ABI 的 so 会报安装失败 9568347,abiFilters 必须双 ABI,每个 ABI 三件套缺一不可;
  12. 签名材料由 DevEco 自动写入本机,仓库内 signingConfigs 保持空数组,证书绝不进入 AtomGit。

8.2 已知限制

  • Compose 绑定层(ModalBottomSheet 渲染、转场动画、NavHost 集成)未编进鸿蒙,由 ArkUI 宿主承担;弹窗手势物理学与 M3 不逐帧一致;
  • confirmValueChange、contentWindowInsets、Modifier、自定义 dragHandle 四个 Composable 参数无法跨 C ABI,引擎规格只保留 hasDragHandle 布尔;
  • KClass 序列化路由(bottomSheet<T>() 重载)依赖 kotlinx-serialization,未纳入引擎规格(字符串 route 已覆盖);
  • SavedState 只做 route 级簿记(savedStateRoutes),不含 Bundle 内容;
  • 同屏多 sheet(上游宿主会把所有可见 entry 逐个组成叠层)Demo 里只展开栈顶一个,其余在状态列表中可见——引擎本身支持任意多个可见 entry;
  • lifecycle op 允许宿主任意设置状态,没有做只进不退校验(与 Compose 宿主的实际驱动方式一致,但宿主要自律);
  • android / ios / js / wasmJs / desktop 目标剥离(composeMain/androidMain/cmpMain 源文件留盘),发布、dokka、detekt 一并剥离,本 fork 不发 Maven Central。

8.3 未来优化方向

  • 跟进社区 CMP/compose.ui 的 ohos 制品线,一旦 composition 宿主可用,评估把 composeMain 接回 ohos 编译(上游 6 个文件逐字节未动,随时可试);
  • 如社区出 kotlinx-coroutines ohos 制品线,把引擎的 listener 换回 StateFlow 对齐上游签名(addListener/removeListener 形状已预留);
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)验收矩阵与自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 compose-material3-navigation(全 Compose + androidx.navigation 绑定)
    → 验证制品缺口(androidx.navigation / material3 无 ohos 变体)
    → 工具链对齐 Kotlin 2.2.21-1.0.0(Gradle 降 8.14.1)
    → 根构建替换(cmp_qrose 模式,conventions 插件链剥离)
    → NavigatorState 语义镜像为纯 Kotlin 引擎(6 文件留档 composeMain + 5 文件引擎)
    → 25 项 JVM 交叉验证逐条钉住
    → C ABI(@CName × 2,单 dispatch)+ 全量状态 JSON
    → C++ N-API(DisposeString 纪律 + 生成头回填)
    → ArkTS 宿主复刻(ModalBottomSheetLayout/Host 编排)
    → ArkUI 真机四 Tab 验收(push / dismiss / popUpTo 批量转场 / 深链 / 返回键策略)

9.2 封装层次

  • 语义层:上游 Navigator + NavigatorState 的纯 Kotlin 镜像(attach 契约、push 双语义、popWithTransition、markTransitionComplete、saveState 簿记、STARTED 过滤、深链匹配);
  • 留档层:上游 6 个 Compose 文件 git mv 进 composeMain,逐字节未动,等工具链对齐接回;
  • Native 层:双 ABI 动态库,单一 C ABI 入口输出 JSON;
  • N-API 层:参数检查、字符串转换、Native 内存释放、生成头同源回填;
  • ArkTS 层:宿主编排(scrim/锚点/拖拽判定/返回键策略)、整体状态刷新、日志与错误文本;
  • DevEco 层:CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 路线选择基于当天的制品可达性:androidx.navigation 无 ohos 变体是硬事实,整库直编、等官方、纯 ArkTS 三条路都被制品与宿主两个事实堵死,"状态机镜像 + 宿主复刻"是唯一自洽解;有 ohos kotlinx 依赖的库走 KStore 的 2.4.20 真编译线,零依赖语义库走本仓的 2.2.21 提取线,不要互相套经验;
  2. 每条语义一个测试:提取引擎最大的风险是"语义漂移",25 项与引擎同步编写的 JVM 测试把上游 NavigatorState 的行为逐条钉死——commonMain 同一份代码喂 JVM 与 ohos,测试全绿就是 so 未漂的唯一可信证明;
  3. 桥的纪律比桥的功能重要:错误不过 FFI、KN 字符串必 DisposeString、生成头不手抄、宿主必补 onTransitionComplete、backstack 真相只在引擎——这些纪律在踩坑表里每一条都对应一次真实的编译失败或状态异常。

9.4 适配成果

当前适配已完成:

  • compose-material3-navigation 0.0.8(fork 0.0.8-ohos.1)导航器状态机的双 ABI Kotlin/Native 编译(上游 6 个 Compose 文件零改动留档,引擎 5 个纯 Kotlin 文件,416 行);
  • 25 项 JVM 交叉验证全绿(逐条钉住 NavigatorState 语义)+ 双 ABI so(arm64 约 2.6MB / x86_64 约 2.2MB)+ 未签名 HAP(约 5.6MB)构建;
  • 2 个 C ABI 符号和 C++ NAPI 包装层(llvm-nm 验证全局 T 符号);
  • DevEco Stage ArkUI 四 Tab 验收页面(导航、Sheet 行为、深链、关于 + 弹窗宿主编排);
  • 真机安装与运行验证(版本握手、注册、push、批量转场、backstack 可视化);
  • 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开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐