/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */
#content_views .toc,
/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */
#content_views.markdown_views > p:empty:has(+ .toc),
#content_views.markdown_views > .toc + p:empty,
/* 富文本旧版目录标记 */
#content_views.htmledit_views #main-toc,
#content_views.htmledit_views #hr-toc,
#content_views.htmledit_views p[id*=\”-toc\”] {
display: none !important;
}
/* 目录去掉后,紧跟的首个标题不再多出一块上边距 */
#content_views.markdown_views > .toc + h1,
#content_views.markdown_views > .toc + h2,
#content_views.markdown_views > .toc + h3,
#content_views.markdown_views > .toc + h4,
#content_views.markdown_views > .toc + p:empty + h1,
#content_views.markdown_views > .toc + p:empty + h2,
#content_views.markdown_views > .toc + p:empty + h3,
#content_views.markdown_views > .toc + p:empty + h4 {
margin-top: 0 !important;
}
本文记录 compose-navigation-reimagined 接入 OpenHarmony 的完整过程,覆盖导航库盘点、纯 Kotlin 引擎语义提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。
compose-navigation-reimagined 是 Jetpack Compose 的类型安全导航库:NavController 管理一个由 NavEntry(唯一 NavId + 目的地)组成的 backstack,navigate / pop / popAll / popUpTo / replaceLast / replaceAll / replaceUpTo / moveToTop / setNewBackstack 覆盖全部栈操作;每个 entry 拥有独立的 Lifecycle / ViewModelStore / SavedStateRegistry,NavScopeSpec 让多个目的地共享作用域化组件,NavAction 驱动 AnimatedNavHost 转场,Parcelable 支撑进程死亡恢复。本次适配与 KStore 的"制品线真编译"路线有本质区别:上游 1.5.0 是纯 Android 库(Kotlin 1.9.0 / AGP 8.1.0 / AndroidX Compose BOM,尚无 KMP 支持),没有 commonMain 可言,耦合点在 android.os.Parcelable、androidx.lifecycle 全家桶与 Compose 运行时——社区 Maven 再齐全的 kotlinx 制品线也解决不了 AndroidX 框架耦合。因此本项目的答案是引擎语义提取移植(Decompose / telephoto / cmp_qrose 同款路线):把 backstack 状态机与 NavHostState 的生命周期/作用域/转场队列语义 1:1 重建为纯 Kotlin commonMain,编译成 OpenHarmony 动态库,ArkUI 页面复刻 AnimatedNavHost 行为作为验收宿主。
项目地址: AtomGit/oh-tpc/compose-navigation-reimagined.git
上游仓库: olshevski/compose-navigation-reimagined
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
compose-navigation-reimagined 在 Android 上卖的是"一个类型安全、可组合、组件作用域正确的导航栈":目的地是任意 Parcelable 类型而不是字符串路由;栈操作是带语义的扩展函数而不是 Fragment 事务;每个栈元素有独立身份,因此拥有自己的生命周期、ViewModelStore 与保存状态;NavScopeSpec 用一行声明就把"这几个页面共享同一个 ViewModelStore"写清楚;AnimatedNavHost 按 NavAction 选择转场,退出页面在转场期间保持存活(outdated 队列),生命周期同步升降级。上游把这套能力全部实现在 Android 源集里,由 Jetpack Compose 直接消费。OpenHarmony 应用同样需要这套导航语义,但上游构建不会生成 OpenHarmony 变体,且社区 Compose 制品线(1.12.0-0.1.0-07)尚无 composition 宿主,Compose UI 代码在鸿蒙上还跑不起来。
如果只把示例页面改成 ArkTS,页面可以画出一摞卡片,却无法证明导航状态机在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证栈操作引擎、生命周期状态机、作用域存储、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。
| 平台耦合 | 28 个核心源文件中 11 个 import android.* / java.* / kotlinx.parcelize:Parcelable 泛型约束贯穿整个 public API,NavId 依赖 java.util.UUID + android.util.Base64,NavHostState 整层站在 Lifecycle / ViewModelStore / SavedStateRegistry 上。 |
| 无 commonMain | 上游 1.5.0 是单一 Android target 的 com.android.library(上游 1.6.0+ 才做多平台),不存在"整体编译 commonMain"的选项。 |
| 工具链不一致 | 上游 Kotlin 1.9.0 / AGP 8.1.0 / Gradle 8.2.1 / buildSrc 预编译脚本插件,与鸿蒙工具链无法共存于一个 Gradle 构建。 |
| Compose UI 编不了 | 社区 ohos Compose 线无 composition 宿主,NavHost / AnimatedNavHost / DialogNavHost / BottomSheetNavHost 的 Composable 无法直接渲染。 |
| 语言边界不同 | ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。 |
| 语义跨不了桥 | mutableStateOf 观察、转场期间的生命周期联动、rememberSaveable 恢复都是 Compose 宿主概念,必须显式化成协议。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:导航引擎的语义提取与 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。栈操作与状态语义完全由移植引擎负责,ArkTS 只负责调用 Native、解析 JSON、按协议驱动转场和展示结果。
1.2 库提供的能力
Demo 提供一个真实的导航会话,对应桥接的全部入口:
- 栈操作(navigate/pop/popAll/popUpTo/replaceLast/replaceAll/replaceUpTo/moveToTop/setNewBackstack):与上游 NavControllerExt 逐字对齐的 9 组操作,含 Match.First / Match.Last 匹配策略与 inclusive 语义;
- 转场协议(onTransitionStart/onTransitionFinish/removeOutdated):上游 AnimatedNavHost 隐式执行的时序被显式化为三个 op,ArkTS 动画严格按 开始 → 动画 → 结束 → 清理 驱动,退出页在转场期间保持存活;
- 作用域存储(entryStore/scopedStore):每个 entry 独立的组件存储 + NavScopeSpec 作用域共享存储,pop 后重进同名页 entry 计数器归零而 scope 计数器保持;
- 宿主生命周期(hostLifecycle):start/resume/pause/stop/destroy 五档驱动全栈生命周期级联(min(host, max) 规则);
- 状态恢复(saveState/restoreState):全量保存栈结构、entry 身份与两级组件存储,模拟进程死亡恢复;
- 版本握手(version):reimaginedVersion() 返回 fork 版本号,来自真实 Native 调用而非静态文字。
响应包含:
| ok / error | 操作是否成功,失败时 error 带原文。 |
| op / version | 回显本次操作与桥版本。 |
| action | 本次栈操作的 NavAction(Navigate / Pop / Replace / Idle),驱动 ArkTS 选择转场。 |
| entries[] | 全量 backstack:id / destination / lifecycle / current / scopes / components。 |
| scopedStores[] / outdated[] | 存活作用域存储与退出动画中的条目。 |
| savedPreview / savedMeta / log[] | 保存态预览、元信息与滚动事件日志。 |
引擎侧完整能力(生命周期状态机、outdated 队列、作用域归属、保存恢复)全部在移植的 commonMain 里随编译进入 .so,JVM 侧保留 36 项 jvmTest 交叉验证用例,证明 ohos .so 里执行的导航逻辑没有漂移。
1.3 实现适配
| 语义复用 | 栈操作扩展函数逐字移植;NavHostState 的生命周期 min 规则、INITIALIZED→STARTED→DESTROYED 跳变规则、outdated 队列、作用域清理时序按上游源码 1:1 重建;不移植的 Android 源文件原样留盘。 |
| 平台目标 | 根构建直接声明 ohosArm64 + ohosX64,保留 jvm() 作交叉验证,:reimagined 出 KLIB,example/nativeApp 出双 ABI 动态库。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);material / material3 / hilt / sample / samples-di / test-utils 模块与 buildSrc 预编译脚本全部留盘不进构建。 |
| 桥接稳定 | 使用 2 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。 |
| 依赖纪律 | 引擎 commonMain 零第三方依赖(纯 Kotlin),Kotlin 钉 2.2.21-1.0.0(eazytec 制品线),36 项交叉验证全绿。 |
| UI 完整 | 四 Tab 覆盖全部桥接入口:栈操作 + 转场复刻、作用域计数器、宿主生命周期 + 日志控制台、状态保存恢复。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。导航语义(什么时候 push、什么时候销毁、什么作用域活着、转场期间谁保持 RESUMED)全部由 Kotlin 引擎裁决,ArkTS 只按引擎返回的状态与协议渲染。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、盘点 Android 耦合点、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、buildSrc 摘出
第 3 阶段:引擎语义提取 ── 栈操作逐字移植、生命周期状态机、outdated 队列、作用域存储、save/restore
第 4 阶段:桥协议建模 ── 扁平 JSON 契约、转场三段协议、错误通道
第 5 阶段:原生桥接 ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主与验证 ── 四 Tab 页面、转场协议驱动、HAP、签名、设备安装、真机运行和效果图
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
reimagined/ 上游核心模块(重写为 KMP:jvm + ohosArm64 + ohosX64)
reimagined/src/commonMain/ 新增:导航引擎(11 个文件,语义移植)
reimagined/src/commonTest/ 新增:36 项交叉验证(上游测试语义移植到 kotlin-test)
reimagined/src/{main,test,androidTest} 上游 Android 源码,原样留盘不编译
reimagined-material*/ reimagined-hilt/ 上游 Material/BottomSheet/Hilt 模块,留盘不进构建
sample/ samples-di/ test-utils/ 上游示例与 DI 演示(Android 工程),留盘不进构建
example/nativeApp/ Kotlin/Native 动态库(Bridge + MiniJson + FullJson)
example/harmonyApp/ DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md 鸿蒙适配说明(中英双份)
README.upstream.md 上游原文保存
docs/ 上游 mkdocs 文档站 + 本博客 + img/ 效果图
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :reimagined 与 :example:nativeApp,rootProject.name = "ohos_compose-navigation-reimagined"。上游的 nexusPublishing / publishing-config 签名发布、AGP classpath、buildSrc 预编译脚本全部随根构建替换剥离——本 fork 不发 Maven Central。
1.2 工具链矩阵
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven) |
| JDK | 21(Temurin 21.0.12,org.gradle.java.home 写入 gradle.properties) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(jvm() 保留作交叉验证) |
| Gradle | 8.14.1(wrapper 从 cmp_qrose 同款替换;2.2.21 旧线必须配 8.x,9.x 起不兼容) |
| 引擎依赖 | commonMain 零第三方依赖(纯 Kotlin);commonTest 仅 kotlin("test") |
| DevEco product | 5.1.0(18) 兼容 |
| DevEco Studio | 26.0.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.reimagined |
$env:JAVA_HOME = "C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot"
java –version
1.3 源码依赖度盘点
上游 reimagined 模块 28 个源文件逐一核对 import 面,分三档处置:
| 纯逻辑 | NavControllerExt / Match / NavScopeSpec / NavBackstack / NavAction / NavEntry 等 | 逐字移植(去掉 @Stable / Parcelable 等平台标注) |
| 平台 API 渗透 | NavId(UUID + Base64 + ByteBuffer)、NavController(Parcelable CREATOR + mutableStateOf)、NavHostState(Lifecycle/ViewModelStore/SavedStateRegistry 全家桶) | 语义重建:NavId 改随机 16 字节 hex;观察改监听器;NavHostState 重建为 NavEngine |
| Compose UI | NavHost / AnimatedNavHost / DialogNavHost / NavBackHandler / 转场三件套 | 留盘不编译,ArkTS 复刻宿主行为 |
expect/actual 一个都不需要——引擎不碰协程、不碰 IO、不碰序列化框架,这是导航状态机天然的优势:它的全部输入输出就是内存里的列表与状态。
1.4 页面边界
标题区 REIMAGINED · OPENHARMONY / 状态行(op · entries · scopes · action 四格实时刷新)
Tab 切换 Stack 栈操作 / Scopes 作用域 / Lifecycle 生命周期 / State 状态恢复
Stack 目的地舞台(当前页卡片 + 按 action 转场)、12 个栈操作按钮全集、
backstack 列表(彩色 lifecycle 徽章 / scopes 标签 / counter / 短 id)、outdated 列表
Scopes entryStore 计数器(±/清零,作用于栈顶)、scopedStore 计数器(shared / own 各自 ±)、
存活作用域列表(scope / counter / lifecycle / 短 id)
Lifecycle hostLifecycle 五按钮(start/resume/pause/stop/destroy)+ 重新 bootstrap、
深色事件日志控制台(引擎事件流,最新在底部自动滚动)
State saveState / restoreState + 便捷改栈按钮、savedMeta 与 preview 截断展示、版本与架构卡
页面启动时先调 bootstrap 建立导航会话(初始 backstack First,scopeRules First=shared|Second=shared|Third=shared+own|Fourth=),每次操作后整页状态由响应全量刷新。
第 2 阶段:目标与依赖打通
2.1 制品线验证(引擎零依赖,钉版只服务工具链)
与 KStore 不同,本引擎 commonMain 没有任何第三方依赖,制品线验证只需要确认一件事:eazytec Maven 上的 HarmonyOS Kotlin 2.2.21-1.0.0 发行版提供 ohosArm64() / ohosX64() target 工厂(qrose / telephoto / sheets-compose-dialogs 已在库模块上验证过这条线)。工具链选择因此回到"跟随已审核先例":
- Kotlin 钉 2.2.21-1.0.0:与 cmp_qrose / telephoto / Decompose(旧线)同款,社区审过、.konan ohos 工具链(sysroot-ohos-26.0.0.38、cpf-llvm)本机齐备;
- Gradle 降 8.14.1:2.2.21 旧线不支持 Gradle 9.x,wrapper 四件套从 cmp_qrose 同款复制;
- 不升 2.4.20 线:升线的唯一理由是依赖的 stdlib 元数据(KStore 的 coroutines 场景),本项目零依赖,无此需求——两条工具链线的经验不能互相套用。
settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
// HarmonyOS Kotlin toolchain (ohosArm64/ohosX64 targets) must come first,
// otherwise the ohosArm64()/ohosX64() factory functions stay unresolved.
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_compose-navigation-reimagined"
include(":reimagined")
include(":example:nativeApp")
2.2 编译引擎(buildSrc 摘出是本阶段的坑)
上游全部模块配置都站在 buildSrc 预编译脚本插件上(android-library-config / publishing-config,内部 apply AGP 8.1.0 + Kotlin 1.9.0 + AndroidX Compose)。Gradle 对 buildSrc 是无条件自动编译的——哪怕 settings 只 include 两个纯 KMP 模块,AGP 8.1.0 / Kotlin 1.9.0 仍会被拉进构建。处置:buildSrc/build.gradle.kts 置空(保留文件与说明注释),上游脚本插件留在 src/main/kotlin 磁盘上但不再被编译和解析。
reimagined/build.gradle.kts 重写为纯 KMP 模块:
plugins {
alias(libs.plugins.kotlin.multiplatform)
}
kotlin {
applyDefaultHierarchyTemplate()
jvmToolchain(21)
jvm() // 交叉验证:commonTest 在 JVM 上跑
ohosArm64()
ohosX64()
sourceSets {
commonMain.dependencies {} // 引擎零依赖
commonTest.dependencies {
implementation(kotlin("test"))
}
}
}
上游 Android 源码在 src/main / src/test / src/androidTest 不注册进任何 source set,原样留盘。实测:
.\\gradlew :reimagined:compileKotlinJvm # BUILD SUCCESSFUL
.\\gradlew :reimagined:compileKotlinOhosArm64 :reimagined:compileKotlinOhosX64 # BUILD SUCCESSFUL
2.3 测试任务的处置
Ohos*Test* 任务在 ohos target 上没有 test runner,根构建全局禁用(qrose / KStore 同款纪律),测试统一在 JVM 跑:
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
第 3 阶段:引擎语义提取(技术难点)
3.1 三类语义的移植策略
① 栈操作语义(逐字移植)
NavControllerExt.kt 9 组扩展 + Match 枚举 —— 函数体是纯列表运算,零改动;
仅 setNewBackstack 的 mutableStateOf 换成监听器列表(observe/unobserve)。
② 身份语义(替换平台实现,保持外部行为)
NavId:java.util.UUID + android.util.Base64 → 随机 16 字节 hex(32 字符),
同样保证唯一性,toString 即存储形式,NavId.of() 支撑恢复路径;
NavHostId:@JvmInline value class → 普通 internal 类(ohos Kotlin/Native
没有 kotlin.jvm.JvmInline,见 8.1 踩坑 2)。
③ 宿主状态语义(1:1 重建,本适配的核心工作量)
NavEngineEntry:每个 entry 的生命周期状态机。hostLifecycleState(宿主档)
与 maxLifecycleState(转场档)取 min 生效;枚举声明序复刻 Android
Lifecycle.State(DESTROYED < INITIALIZED < CREATED < STARTED < RESUMED),
minOf 因此与上游语义完全一致;INITIALIZED→DESTROYED 必须经停 STARTED;
组件存储 Map<String,String> 替代 ViewModelStore + SavedStateRegistry。
NavEngine:上游 NavHostStateImpl 的移植。createSnapshot 产出快照并把离开
backstack 的 entry 排进 outdated 队列(转场期间存活);onTransitionStart
把不可见 entry 压到 STARTED、onTransitionFinish 把可见升 RESUMED 其余压
CREATED、removeOutdatedHostEntries 才 DESTROY 并清空组件存储;作用域
entry 以 scope 为键 getOrPut,最后一个关联 entry 离栈时随之 outdated;
saveState/restoreState 以全字符串字段替代 Parcelable 恢复。
3.2 转场协议显式化(导航库区别于存储库的适配点)
上游 AnimatedNavHost 在 Compose 里隐式完成"新快照渲染 → 转场开始 → 动画 → 转场结束 → 清理 outdated",生命周期联动藏在组合层。桥协议必须把这段时序变成显式契约:
ArkTS 收到操作响应(含新 backstack 快照与 action)
→ 渲染目标态,调用 op=onTransitionStart (不可见 entry 压 STARTED)
→ 播放转场动画(Navigate 右滑入 / Pop 左滑出 / Replace 交叉淡化)
→ 调用 op=onTransitionFinish (可见升 RESUMED,其余压 CREATED)
→ 调用 op=removeOutdated (退出 entry DESTROY,组件存储清空)
快速连续操作打断上一次转场时,ArkTS 侧补发 onTransitionFinish + removeOutdated 再开新转场——引擎侧 outdated 队列支持多个快照排队(上游同款 ArrayDeque 结构),不会提前销毁还在动画中的 entry。
3.3 save/restore:Parcelable 的 JSON 映射
上游进程死亡恢复靠 NavController Parcelable(entry 顺序 + 去重目的地 + action)+ NavHostSavedState(hostId、entry ids、scoped records、outdated ids)。移植引擎把两者合并为一个全字符串的 NavEngineSavedState,桥上序列化为一个 JSON 对象(hostId / action / entries[] / hostEntryIds[] / scopedRecords[] / outdatedHostEntryIds[] / hostEntryStores{} / scopedStores{}),恢复时用 entry id 重建 NavEntry(身份保持),引擎按"backstack 里还在的恢复组件存储、不在的移除"的上游规则重建现场。
第 4 阶段:桥协议建模
4.1 JSON 契约
dev.olshevski.navigation.reimagined/**(移植引擎,原样编译)
↓ NavController / NavEngine / 生命周期状态机 / 作用域存储
Bridge.kt(@CName × 2,扁平 JSON 进出)
↓ UTF-8 JSON
N-API reimaginedVersion() / reimaginedDispatch()
↓
ArkUI Index.ets(四 Tab)
请求与响应:
请求 : {"op":"navigate","destinations":"Second,Third"}
{"op":"popUpTo","destination":"First","inclusive":"false","match":"last"}
{"op":"scopedStore","scope":"shared","action":"inc"}
{"op":"hostLifecycle","event":"pause"}
{"op":"restoreState"}
响应 : {"ok":true,"op":"navigate","version":"1.5.0-ohos.1","hasWorld":true,
"action":"Navigate","entries":[…],"scopedStores":[…],"outdated":[…],
"savedPreview":null,"savedMeta":"","log":["#12 backstack -> 2 entries …"]}
{"ok":false,"error":"no world yet; call bootstrap first"}
不对称设计照搬 KStore 的经验:请求是扁平 JSON(MiniJson,键映射标量字符串),列表类参数用逗号分隔字符串编码(destinations / scopeRules / keep);响应是完整 JSON,ArkTS 侧 JSON.parse 一次到位,嵌套对象(entries[].components)原生嵌入不再转义。20 个 op 分三组:3 个会话(version / status / bootstrap)、9 个栈操作、8 个协议与状态(转场三段、entryStore / scopedStore、hostLifecycle、saveState / restoreState)。
4.2 scopeRules:把 NavScopeSpec 函数值变成可传输数据
NavScopeSpec 上游是 fun interface(Kotlin 函数值),跨不了 C ABI。桥把作用域声明收敛为声明式规则串:"First=shared|Second=shared|Third=shared+own|Fourth="(目的地 = 作用域 1 + 作用域 2,多条用 | 分隔,空集即 EmptyScopeSpec)。引擎侧仍是真正的 NavScopeSpec 实例——桥在 Kotlin 侧查规则表返回 Set<S>,上游的"作用域 = 返回值集合"语义原样保留,ArkTS 只是规则的书写者。
4.3 错误通道
每个导出整体 try/catch,Kotlin 异常永远不跨 FFI:
@CName("ReimaginedDispatch")
fun reimaginedDispatch(request: String): String = try {
val json = MiniJson.parse(request)
dispatch(json.str("op"), json)
} catch (t: Throwable) {
JsonOut()
.bool("ok", false)
.str("error", t.message ?: t.toString())
.build()
}
非法 JSON、缺 op、未 bootstrap 就操作、空栈上取栈顶存储、不认识的 op、saveState 前先 restore,全部走同一个 ok:false 通道,ArkTS 侧统一渲染红字。
第 5 阶段:原生桥接(技术难点)
5.1 三层桥接
ArkTS
│ JSON string
▼
C++ N-API entry(libreimagined_napi.so)
│ const char*
▼
Kotlin/Native C ABI(libohosreimagined.so)
│ ReimaginedDispatch / ReimaginedVersion
▼
UTF-8 JSON + DisposeString
5.2 Kotlin/Native 导出函数
@CName("ReimaginedDispatch")
fun reimaginedDispatch(request: String): String // 20 op 一口进
@CName("ReimaginedVersion")
fun reimaginedVersion(): String = "1.5.0-ohos.1" // 版本握手
| ReimaginedDispatch | reimaginedDispatch(req) | bootstrap 建会话 + 9 组栈操作 + 转场三段 + 两级存储 + 宿主生命周期 + save/restore |
| ReimaginedVersion | reimaginedVersion() | fork 版本握手(真机版本行的数据来源) |
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
{"reimaginedDispatch", nullptr, NapiReimaginedDispatch, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
{"reimaginedVersion", nullptr, NapiReimaginedVersion, nullptr, nullptr, nullptr,
static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};
每个回调统一三步:napi_typeof 校验必须是 string、OH_LOG_INFO 打字节数、调 KN 函数并包装返回。KN 返回的字符串必须经 libohosreimagined_symbols()->DisposeString 归还 Kotlin 运行时,绝不 free();API 26 的 napi_get_value_string_utf8 非 const 缓冲区用 std::vector<char> 规避。libohosreimagined_api.h 按社区已审仓库同款手写(只声明 ExportedSymbols 结构与两个业务符号)。
5.4 CMake 和类型声明
add_library(ohosreimagined SHARED IMPORTED)
set_target_properties(ohosreimagined PROPERTIES
IMPORTED_LOCATION ${KN_SO}
IMPORTED_NO_SONAME TRUE)
add_library(reimagined_napi SHARED napi_init.cpp)
target_link_libraries(reimagined_napi PUBLIC
libace_napi.z.so
libhilog_ndk.z.so
ohosreimagined)
OHOS_ARCH 为空或 libohosreimagined.so 缺失直接 FATAL_ERROR(提示先跑 link 任务);$ORIGIN rpath 加 POST_BUILD 拷贝保证 NAPI so、KN so、libc++ 三件套同目录,dlopen 时互相可见。
export const reimaginedDispatch: (request: string) => string;
export const reimaginedVersion: () => string;
第 6 阶段:ArkUI 宿主与验证
6.1 工程结构
example/
├── nativeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=ohosreimagined)+ cxxShared + copyKnSo*
│ └── src/ohosMain/kotlin/org/terminator/ohos/reimagined/
│ ├── Bridge.kt @CName × 2 + MiniJson(扁平 JSON,继承自 cmp_qrose 审核模式)
│ └── Json.kt JsonOut 写出器 + FullJson 递归解析(restore 路径专用)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(签名由 DevEco 自动生成回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.reimagined、versionName 1.5.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/libohosreimagined_api.h
├── src/main/cpp/types/libreimagined_napi/{index.d.ts, oh-package.json5}
└── src/main/ets/pages/Index.ets ArkUI 四 Tab 验收页面(1393 行)
6.2 构建、签名和安装
.\\gradlew :reimagined:jvmTest # 36 项交叉验证
.\\gradlew :example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64 # 双 ABI so + 自动回填 entry/libs
cd example\\harmonyApp
hvigorw assembleHap –p product=default –p buildMode=debug
签名在 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.reimagined –m entry
四、完整代码对照
4.1 整体架构
dev.olshevski.navigation.reimagined/**(移植引擎,11 个 commonMain 文件)
│ NavEntry / NavId / NavBackstack / NavAction / Match
│ NavController + 9 组栈操作扩展(逐字移植)
│ NavEngine + NavEngineEntry(生命周期状态机 / outdated 队列 / 作用域存储 / save-restore)
Bridge.kt + MiniJson + JsonOut + FullJson
│ ReimaginedDispatch / ReimaginedVersion
libreimagined_napi.so
│ reimaginedDispatch / reimaginedVersion
Index.ets
│ JSON parse + 转场三段协议驱动 + @State 更新
▼
ArkUI 真机页面(四 Tab)
4.2 文件清单
| settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_compose-navigation-reimagined、Android 模块移出 |
| gradle/libs.versions.toml | Kotlin 2.2.21-1.0.0 钉版 + multiplatform 插件别名(唯一依赖声明) |
| build.gradle.kts(根) | 版本 1.5.0-ohos.1、全局禁用 Ohos*Test* 任务 |
| buildSrc/build.gradle.kts | 置空摘出(上游预编译脚本留盘不编译) |
| reimagined/build.gradle.kts | 纯 KMP:jvm + ohosArm64 + ohosX64,commonMain 零依赖 |
| reimagined/src/commonMain/**(11 文件) | 移植引擎(见 4.3 API 对照) |
| reimagined/src/commonTest/**(2 文件) | 36 项交叉验证(上游测试语义移植到 kotlin-test) |
| example/nativeApp/build.gradle.kts | 双 ABI sharedLib、cxxShared 定位 libc++、copyKnSo* 回填 |
| nativeApp/…/Bridge.kt | 2 个 C ABI、20 op 分发、MiniJson、错误通道 |
| nativeApp/…/Json.kt | JsonOut 手写序列化 + FullJson 递归解析(saveState 往返) |
| 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 四 Tab 验收页面 + 转场协议驱动 |
| README.OpenHarmony_CN.md | 鸿蒙适配主文档(26 节) |
4.3 关键 API 对照
| 引擎 | navController(initialBackstack) | 建控制器,栈序自底向顶(对应桥 bootstrap) |
| 引擎 | navigate / pop / popAll / popUpTo / replaceLast / replaceAll / replaceUpTo / moveToTop | 9 组栈操作扩展,与上游逐字对齐(对应同名 op) |
| 引擎 | setNewBackstack(entries, action) | 自定义重排(对应 setNewBackstack op 的 keep + destinations + action) |
| 引擎 | NavController.observe(listener) | backstack 变化订阅(替代 Compose mutableStateOf 观察) |
| 引擎 | NavEngine.createSnapshot() | 产出快照并将离栈 entry 排进 outdated 队列 |
| 引擎 | NavEngine.onTransitionStart / onTransitionFinish / removeOutdatedHostEntries | 转场三段协议(对应三个同名 op) |
| 引擎 | NavEngine.setHostLifecycleState | 宿主生命周期注入(对应 hostLifecycle op 五档) |
| 引擎 | NavEngine.saveState / NavEngineSavedState | 全量状态保存与恢复(对应 saveState / restoreState op) |
| 引擎 | hostEntryStore(id) / scopedEntryStore(scope) | 两级组件存储(对应 entryStore / scopedStore op) |
| Native | ReimaginedDispatch / ReimaginedVersion | C ABI JSON 导出 |
| N-API | reimaginedDispatch(req) / reimaginedVersion() | 向 ArkTS 暴露能力 |
| ArkTS | send(op, extra) | 触发操作、驱动转场协议并刷新 UI |
4.4 ArkTS 与 Kotlin 的边界
const req = new BridgeRequest('navigate');
req.destinations = 'Second';
const res = JSON.parse(reimaginedDispatch(JSON.stringify(req))) as BridgeResponse;
// res.action === 'Navigate';播放右滑入转场前后调
// onTransitionStart → 动画 → onTransitionFinish → removeOutdated
"navigate" -> alive().first.navigate(parseList(json.str("destinations")))
"popUpTo" -> alive().first.popUpTo(
inclusive = json.bool("inclusive"),
match = parseMatch(json.str("match"))
) { it == json.str("destination") }
两者之间只传输扁平 JSON 字符串,不传输 Kotlin 对象或未校验的动态结构。
五、关键决策说明
决策 1:引擎语义提取而不是等上游多平台版本
上游 1.5.0 没有 commonMain,material / hilt / sample 八个模块整体绑定 AGP;上游 1.6.0+ 虽做了 KMP 化,但版本断代(依赖链大改)且 Compose UI 在鸿蒙仍无宿主——等版本解决不了"框架耦合 + UI 无宿主"两个根本问题。提取语义的好处是边界清晰:栈操作逐字移植保证行为一致,宿主语义 1:1 重建保证可验证,Android 源文件零改动留盘保证可追溯。
决策 2:工具链钉 2.2.21-1.0.0,Gradle 降 8.14.1
引擎零第三方依赖,不存在"依赖制品线反推 Kotlin 版本"的压力(KStore 的场景),钉版只服务工具链本身:2.2.21 线与 cmp_qrose / telephoto / Decompose(旧线)一致,社区已审核;该线不支持 Gradle 9.x,wrapper 从 cmp_qrose 复制 8.14.1 四件套。JDK 21(Temurin)写入 gradle.properties 的 org.gradle.java.home,Windows 上 gradlew.bat 启动器另需 JAVA_HOME 环境变量。
决策 3:根构建替换(cmp_qrose 模式)+ buildSrc 摘出 + Android 模块留盘
上游根构建(nexusPublishing / publishing-config 签名发布 / AGP classpath / buildSrc 预编译脚本)对鸿蒙没有保留价值,整个替换为极简 ohos 构建。material / material3 / material-common(BottomSheetNavHost,绑定 AndroidX Material)、hilt(绑定 Dagger)、sample / samples-di(Android 应用)留盘不进 settings.gradle.kts——它们的存在方式只有这一种。buildSrc 因 Gradle 无条件自动编译而必须置空,这是 Android 库改 KMP 时和"改 settings"同级的必做步骤。
决策 4:转场协议显式化 + 组件存储字符串化
导航库的适配难点不在数据(栈就是列表)而在时序:转场期间谁存活、谁降级、何时销毁。把 AnimatedNavHost 的隐式时序显式化为三个 op,让 ArkTS 动画严格照协议驱动,是"引擎裁决语义、ArkTS 负责渲染"边界的成立条件。组件存储用 Map<String, String> 替代 ViewModelStore/SavedStateRegistry:值跨 JSON 桥不变形,"存储活着多久"的语义与上游完全一致(entry 存活期内有效、scope 最后关联 entry 离栈即清空)。
决策 5:JVM 交叉验证 + 测试语义移植
引擎逻辑在 commonMain,JVM 与 ohos 跑同一份代码;36 项 jvmTest 是"内核没漂"的闸门。与 KStore"上游测试零改动"不同:上游测试绑定 JUnit5 + Truth + Android instrumentation(Parcelable CREATOR、ActivityScenario),不可能在 KMP commonTest 里编译——处置是把上游测试的断言语义移植到 kotlin-test(含 Match.Last 在重复目的地栈上的行为、setNewBackstack 外部修改保护、pop 空栈不动作为假等易错点),并为引擎新增转场协议全序列、作用域存活、save/restore 往返等导航特有的用例。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(x64)、JDK 21(Temurin 21.0.12)、DevEco Studio 26.0、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64,手动签名安装)。
6.2 JVM 交叉验证
.\\gradlew :reimagined:jvmTest
36 项用例全部通过(failures = 0):
| NavControllerExtTest | 25 | 9 组栈操作语义(含 Match.First / Match.Last 在重复目的地栈上的差异、inclusive 边界、moveToTop 保持 entry 身份、空栈行为、setNewBackstack 外部修改保护、observe 退订、NavId 字符串往返) |
| NavEngineTest | 11 | 转场协议全序列(pop 退出动画 RESUMED→STARTED→CREATED→DESTROYED)、作用域存储共享与销毁时序、entry 存储存活、宿主生命周期 min 级联、INITIALIZED→DESTROYED 跳变、save/restore 往返(身份与组件存储保持)、clear、快照结构 |
6.3 HAP 验证
:reimagined:compileKotlinJvm: BUILD SUCCESSFUL
:reimagined:jvmTest: BUILD SUCCESSFUL(36/36)
:reimagined:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64 / linkDebugSharedOhosX64: BUILD SUCCESSFUL
llvm-nm -D: ReimaginedDispatch / ReimaginedVersion / libohosreimagined_symbols 均为全局 T 符号
BuildNativeWithCmake: finished(双 ABI 编出 libreimagined_napi.so)
CompileArkTS: finished
PackageHap: finished
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
entry-default-unsigned.hap 约 6.1 MB,内含双 ABI 各三件套:arm64-v8a 的 libreimagined_napi.so 约 65KB + libohosreimagined.so 约 1.6MB + libc++_shared.so 约 1.3MB,x86_64 的约 67KB + 1.4MB + 1.3MB(HAP 内 strip 后;debugShared 未 strip 时 KN so 约 2.98MB / 2.63MB,含 Kotlin/Native 运行时)。
6.4 功能用例
6.5 验证结论
栈操作引擎、生命周期状态机、作用域存储、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的导航会话:栈操作全集、转场协议、作用域存活与销毁、宿主生命周期级联和状态恢复闭环(见下节效果图)。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/compose-navigation-reimagined.git
cd compose-navigation-reimagined
$env:JAVA_HOME = "C:\\Program Files\\Eclipse Adoptium\\jdk-21.0.12.101-hotspot"
.\\gradlew :reimagined:compileKotlinOhosArm64 :reimagined:compileKotlinOhosX64 # klib
.\\gradlew :reimagined:jvmTest # 36 项交叉验证
.\\gradlew :example:nativeApp:linkDebugSharedOhosArm64 `
:example:nativeApp:linkDebugSharedOhosX64 # 双 ABI so 回填
cd example\\harmonyApp
hvigorw assembleHap –p product=default –p buildMode=debug
hdc install –r entry\\build\\default\\outputs\\default\\entry-default-signed.hap
hdc shell aa start –a EntryAbility –b org.terminator.ohos.reimagined –m entry
hdc shell hilog | findstr Reimagined
八、遗留问题与改进方向
8.1 踩坑复盘
8.2 已知问题
- Compose UI(NavHost / AnimatedNavHost / DialogNavHost / BottomSheetNavHost / NavBackHandler)未移植:社区 ohos Compose 线(1.12.0-0.1.0-07)尚无 composition 宿主,转场行为由 ArkUI 复刻,语义由引擎裁决;
- reimagined-material / reimagined-material3 / reimagined-material-common(BottomSheetNavHost 三件)与 reimagined-hilt 留盘未适配(绑定 AndroidX Material / Dagger);
- NavScopeSpec 是 Kotlin 函数值,跨不了 C ABI:桥上退化为声明式 scopeRules 字符串(目的地精确匹配),上游任意谓词的作用域判定留在 Kotlin 侧;
- Parcelable 进程死亡恢复映射为显式 saveState / restoreState op:语义等价(entry 身份与组件存储保持),触发时机从系统托管变为手动/宿主托管;
- 桥持有单导航会话(一个 controller + engine world),多 NavHost 嵌套(上游 nested navigation)未在桥上建模;
- 宿主生命周期由 ArkTS 手动驱动(hostLifecycle op),未接系统应用生命周期回调;
- 深链(上游 sample 的 deeplink intent-filter)未演示;
- android 侧全部目标与源文件留盘不进构建,发布、签名发布链路剥离,本 fork 不发 Maven Central;HAP 安装必须使用与 bundle name org.terminator.ohos.reimagined 匹配的签名。
8.3 未来优化方向
- 接入系统应用生命周期回调自动驱动 hostLifecycle,替代手动按钮;
- 把 NavController.observe 的状态推送跨 N-API 回表(线程安全回调),替代操作后读回;
- 复刻 DialogNavHost / BottomSheetNavHost 的 ArkUI 宿主(弹层导航 + Material 动效);
- 跟进上游 1.6+ 多平台版本,评估 commonMain 直接真编译替代手工提取的可行性;
- 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
- 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补充真机(x86_64)验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 reimagined(纯 Android,28 文件)
→ 依赖度盘点(11 文件平台耦合分档处置)
→ 工具链对齐 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(cmp_qrose 先例线)
→ 根构建替换 + buildSrc 摘出(Android 模块留盘)
→ 引擎语义提取(栈操作逐字 + 宿主状态 1:1 重建)
→ 转场协议显式化(三段 op 时序契约)
→ C ABI(@CName × 2)+ 扁平 JSON
→ C++ N-API(DisposeString 纪律)
→ ArkTS JSON 解析 + 转场协议驱动
→ ArkUI 真机四 Tab 验收(栈操作 / 作用域 / 生命周期 / 状态恢复)
9.2 封装层次
- 引擎层:NavEntry / NavId / NavBackstack / NavAction / Match 与 9 组栈操作扩展(上游逐字移植);
- 宿主状态层:NavEngine + NavEngineEntry——生命周期 min 状态机、outdated 转场队列、作用域归属与清理、save/restore(上游 NavHostStateImpl 语义重建);
- Native 层:生成双 ABI 动态库并通过 2 个 C ABI 输出 JSON;
- N-API 层:完成参数检查、字符串转换和 Native 内存释放;
- ArkTS 层:管理页面状态、JSON 校验、操作分发、转场协议驱动、日志与错误文本;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
9.4 适配成果
当前适配已完成:
- compose-navigation-reimagined 1.5.0 导航引擎的双 ABI Kotlin/Native 编译(栈操作逐字移植 + NavHostState 语义 1:1 重建,上游 Android 源码零改动留盘);
- 2 个 C ABI 符号和 C++ N-API 包装层(llvm-nm 验证全局 T 符号);
- 20 op 扁平 JSON 桥协议(9 组栈操作 + 转场三段协议 + 两级组件存储 + 宿主生命周期 + save/restore);
- DevEco Stage ArkUI 四 Tab 验收页面(栈操作转场复刻、作用域计数器、生命周期级联、状态恢复,1393 行);
- 36 项 JVM 交叉验证全绿 + HAP 构建(双 ABI 六件套 so,约 6.1MB)、签名、设备安装和真机验证;
- AtomGit 项目文档、OpenHarmony README(中英双份、中文主文档 26 节)和本地效果图。
适配过程 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。
参考文档
- AtomGit/oh-tpc/compose-navigation-reimagined
- KMP&CMP 鸿蒙社区
- 上游 compose-navigation-reimagined(GitHub)
- Kotlin Multiplatform
- OpenHarmony N-API
- DevEco Studio
- OpenHarmony ArkUI
网硕互联帮助中心



评论前必须登录!
注册