开源鸿蒙平台 CMP 三方库「compose-material3-navigation」适配全流程
本文记录
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 Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin jdk-21.0.12.101-hotspot,org.gradle.java.home 固定写入) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 从上游 9.2.1 降级,2.2.21 旧线纪律) |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 26.0.0.851 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.m3navigation |
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
| 上游文件(commonMain) | 依赖形态 | 处置 |
|---|---|---|
ModalBottomSheetNavigator.kt | expect class + NavigatorState + Material3 类型 | 语义下沉引擎;原文件 git mv → composeMain |
ModalBottomSheetLayout.kt | Compose 宿主入口 | git mv → composeMain;ArkTS Stack 布局承接 |
ModalBottomSheetHost.kt | 可见列表过滤、BackHandler、转场回调 | git mv → composeMain;ArkTS syncSheetFromEngine() 承接 |
ModalBottomSheetNavGraphBuilder.kt | bottomSheet() DSL | git mv → composeMain;桥 op=register 承接 |
ModalBottomSheetNavigatorDestinationBuilder.kt | NavDestinationBuilder 子类 | git mv → composeMain;SheetDestinationSpec 承接 |
BackHandler.kt | internal expect fun | git 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 返回空,导航抛 IllegalStateException | backStackReadsEmptyBeforeAttach |
navigate(entries) → state.push(CMP actual) | navigate(route, args, withTransition=false) | cmpPushDoesNotEnterTransition |
Android actual 的 state.pushWithTransition | navigate(..., withTransition=true) | androidPushWithTransitionEntersTransition |
dismiss(entry) → popBackStack(entry, false) → popWithTransition | dismiss(entry):出栈 + 进 transitionsInProgress | dismissRemovesFromBackStackAndMarksTransition |
state.markTransitionComplete(entry) | onTransitionComplete(entry) | onTransitionCompleteDrainsTransition |
popBackStack(popUpTo, savedState) 连 popUpTo 一起弹 | 弹到栈底方向全部出栈并批量转场 | popBackStackPopsUpToAndIncluding |
NavOptions.saveState/restoreState | savedStateRoutes 簿记,navigate 消费 | saveStateIsConsumedByLaterNavigate |
rememberVisibleList:Lifecycle ≥ STARTED 过滤 | visibleEntries() | visibleEntriesFilterNonStartedEntries |
| NavController 深链匹配 | deepLink(uri):{placeholder} 路径段 + query | 3 项深链测试 |
实测:
./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 方法 | 覆盖内核 |
|---|---|---|
M3NavDispatch | m3NavDispatch(req) | 引擎注册表 + backstack + 转场 + 生命周期 + 深链 |
M3NavVersion | m3NavVersion() | 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 entry | syncSheetFromEngine() 只组 visible=true 的栈顶 entry |
push 后 CREATED,组合后 STARTED,展开后 RESUMED | navigate 成功 → op=lifecycle started → 展开动画后 → resumed |
ModalBottomSheet(onDismissRequest = { dismiss(entry) }) | scrim 点击 / 拖拽越阈值 → op=dismiss |
sheet 锚点(half/full)与 skipPartiallyExpanded | 本页锚点状态机:skip 目的地只有 full/hidden 两个锚 |
dragHandle ?: null | Rect 拖拽条 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.properties | Gradle 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.kt | 2 个 C ABI、op 分发、状态序列化、滚动日志、错误通道 |
nativeApp/.../Json.kt | 手写递归下降 JSON 解析 + JsonWriter(零依赖) |
harmonyApp/.../cpp/napi_init.cpp | N-API 导出、参数检查、DisposeString |
harmonyApp/.../cpp/CMakeLists.txt | IMPORTED 引入 KN so、$ORIGIN rpath、POST_BUILD 三件套 |
harmonyApp/.../ets/pages/Index.ets | ArkUI 验收页面(宿主复刻) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(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) |
| 上游 | ModalBottomSheetProperties | SheetPropertiesSpec 四布尔(op=register 载荷) |
| Native | M3NavDispatch / M3NavVersion | C ABI JSON 导出 |
| N-API | m3NavDispatch(req) / m3NavVersion() | 向 ArkTS 暴露能力 |
| ArkTS | dispatch(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、detachReadsEmptyAndBlocksNavigation | NAME="modalBottomSheet";attach 前 backStack 空读、导航拒绝 |
| push 语义 | navigatePushesEntriesInOrder、cmpPushDoesNotEnterTransition、androidPushWithTransitionEntersTransition | 栈序、自增 id;CMP 无转场 vs Android 有转场 |
| 出栈与转场 | dismissRemovesFromBackStackAndMarksTransition、popBackStackPopsUpToAndIncluding、onTransitionCompleteDrainsTransition、popPopsTheTopEntry | dismiss=popWithTransition;popUpTo 连带弹出;转场排空 |
| saveState | saveStateIsConsumedByLaterNavigate | 簿记 + navigate 消费 |
| 可见性与生命周期 | visibleEntriesFilterNonStartedEntries | STARTED 过滤(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 功能用例
- 启动后"关于"Tab 与导航 Tab 顶部出现
版本 0.0.8-ohos.1(来自真实 Native 调用,非静态文字); - 引导序列在日志区滚动留痕:
engine reset → registerDestination(details/locked/share) → attach(); - 点
navigate(details),sheet 以半展开锚点弹出,日志出现navigate(details) -> id=N [push],backStack 列表实时渲染 entry 的 id/route/lifecycle/visible; - 弹窗展开后 entry 生命周期推进为 RESUMED(宿主发的
op=lifecycle); - 下拉越过阈值或点 scrim,sheet 隐藏,
transitionsInProgress出现该 entry id,隐藏动画结束补发转场完成后清空; - 连续 push 多个目的地后点
popUpTo(home),栈顶方向全部 entry 批量出栈并进入transitionsInProgress(真机会话中观察到[4, 3, 2, 1, 5, 8]的批量转场列表); - 打开
locked目的地:skip 直接全展开、无拖拽条、shouldSecureCanvas=true;点"模拟返回键"被策略拒绝并提示; - 深链 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 踩坑复盘
- 只改 ArkTS 页面不算 KMP 适配,必须让
:library真正编译成ohosArm64/ohosX64动态库,且语义有测试钉住; settings.gradle.kts里 eazytec 双仓必须排在pluginManagement与dependencyResolutionManagement最前,否则配置期直接Unresolved reference: ohosArm64;- 上游根构建挂在
com.eygraber.conventions*约定插件上,根构建替换时整个插件链(AGP / Compose / 发布)一并剥离——gradle.properties里残留的 Android/Compose 属性也要同步清掉; - commonMain 里不能用 JVM 专属 API:
putIfAbsent是 JDK 8 Map 默认方法、kotlin.synchronized是 JVM 专属,编译 ohos 时才报Unresolved reference——改手写containsKey判断;锁直接删掉,ArkTS 单线程调用无需加锁; - 表达式函数体(
= synchronized { ... })改块体时容易丢return,报Missing return statement; rememberVisibleList的语义是"STARTED 才可见"——测试断言"push 后立即可见"是错的,不是引擎 bug;push 后 entry 是 CREATED,必须由宿主推进生命周期;- ArkTS(严格模式)不支持对象展开
{ ...obj }与!非空断言,重建对象用显式字段拷贝;.flexWrap是 Flex 容器属性,Row 上没有,编译期直接报Property 'flexWrap' does not exist on type 'RowAttribute'; - NAPI 头(
libohosm3nav_api.h)是 KN 链接产物:不要手抄,用copyKnHeader任务挂finalizedBy自动回填,生成头与.so永远同源; - NAPI 层字符串纪律:
napi_get_value_string_utf8的缓冲区在 API 26 头文件里是非 const,走std::vector中转;KN 返回串必须DisposeString,不能free(); hvigorw assembleHap的打包步(PackageHap)要 spawnjava,命令行构建时必须把 DevEco 自带 JBR 加进 PATH,否则报spawn java ENOENT(错误码 00308018);- HAP 缺当前 ABI 的 so 会报安装失败
9568347,abiFilters必须双 ABI,每个 ABI 三件套缺一不可; - 签名材料由 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;
lifecycleop 允许宿主任意设置状态,没有做只进不退校验(与 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 三条经验
- 路线选择基于当天的制品可达性:
androidx.navigation无 ohos 变体是硬事实,整库直编、等官方、纯 ArkTS 三条路都被制品与宿主两个事实堵死,"状态机镜像 + 宿主复刻"是唯一自洽解;有 ohos kotlinx 依赖的库走 KStore 的 2.4.20 真编译线,零依赖语义库走本仓的 2.2.21 提取线,不要互相套经验; - 每条语义一个测试:提取引擎最大的风险是"语义漂移",25 项与引擎同步编写的 JVM 测试把上游
NavigatorState的行为逐条钉死——commonMain同一份代码喂 JVM 与 ohos,测试全绿就是 so 未漂的唯一可信证明; - 桥的纪律比桥的功能重要:错误不过 FFI、KN 字符串必 DisposeString、生成头不手抄、宿主必补
onTransitionComplete、backstack 真相只在引擎——这些纪律在踩坑表里每一条都对应一次真实的编译失败或状态异常。
9.4 适配成果
当前适配已完成:
- compose-material3-navigation
0.0.8(fork0.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。
参考文档
更多推荐

所有评论(0)