本文记录 navigation-compose-extended 接入 OpenHarmony 的完整过程,覆盖导航库模块盘点、Compose 依赖隔离、路由模型纯化、真实引擎编译、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建和真机验收。

navigation-compose-extended 是 AndroidX Jetpack Navigation Compose 的 Compose Multiplatform 增强库:用 NavDestination<K> 声明类型安全的目标页,@NavDestination 注解配合 KSP 编译期生成 NavArgumentKey 枚举、目标页对象与 SafeNavArgs,NavRoute 把必选参数编进路径段、可选/可空参数编进 query 并应用默认值,NavHost / NavGraphBuilder / rememberNavAction 在 Compose 层把它们接到 androidx.navigation。本次适配与 KStore 的"真编译整库"(社区 Maven 已有完整 HarmonyOS kotlinx 制品线)有本质区别:本库公共 API 深度绑定 androidx.navigation 与 Compose 运行时(上游 Compose 1.12.2 / Kotlin 2.4.21 / navigation 2.9.2,社区鸿蒙线是 CMP 1.9.2 / HarmonyOS Kotlin 2.2.21,两条工具链对不齐),因此走 cmp_qrose 已审核路线——路由引擎(NavDestination 路由模板生成、NavRoute 参数编码、NavArgs 类型化解码语义)的算法逐行保留并编成 OpenHarmony 动态库,Compose UI 四文件隔离进 composeMain 保留不编译,返回栈 / singleTop / popUpTo / 深链匹配由 @CName 桥后的 NavEngine 在真机执行,ArkUI 四 Tab 页面作为验收宿主。

项目地址: AtomGit/oh-tpc/navigation-compose-extended.git

上游仓库: serbelga/navigation-compose-extended

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

navigation-compose-extended 为 CMP 应用提供"类型安全导航"这一件事的完整答案:object HomeNavDestination : NavDestination<NavArgumentKey>() 一行声明目标页,argumentsMap 里按 NavArgumentBuilder DSL 注册参数(类型、可空、默认值),route 自动生成 searchresult/{search}?category={category} 这样的模板,navRoute(SearchNavArgumentKey to "qrose") 一行生成带值路由,NavArgs 在目标页侧按 getIntOrNull / getStringOrDefault 等类型化取值,@NavDestination 注解 + KSP 把这一切从手写变成编译期生成。KMP 项目把这些能力放在 commonMain,由 Android、iOS、桌面共享。OpenHarmony 应用同样需要这套类型安全导航,但上游构建(Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1 / Compose 1.12.2 / navigation 2.9.2)不会生成 OpenHarmony 变体。

如果只把示例页面改成 ArkTS,页面可以显示按钮,却无法证明路由引擎在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证路由模型、参数编解码、返回栈语义、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 交付链路。

障碍具体问题
目标缺失必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony KLIB。
工具链不一致上游 Kotlin 2.4.21 / Gradle 9.8.1 / AGP 9.4.1(com.android.kotlin.multiplatform.library)与鸿蒙工具链无法共存于一个 Gradle 构建;鸿蒙线钉 Kotlin 2.2.21-1.0.0,Gradle 必须降到 8.14.1。
依赖矩阵库的 public API 站在 androidx.navigation(NamedNavArgument / NavArgumentBuilder / NavBackStackEntry / NavHostController)与 Compose runtime 上;社区 Maven 只有 CMP 1.9.2 线的导航制品,上游 navigation 2.9.2 无 ohos 变体可解析。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
导航语义跨不了桥rememberNavAction 是 @Composable、NavHostController 是 Compose/Activity 侧概念,C ABI 只能同步一来一回。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在:路由引擎与注解词汇表的 ohos 编译、根构建替换(cmp_qrose 模式)和 C ABI/N-API 桥接层。模板生成与参数编解码完全由上游原版算法负责,ArkTS 只负责注册声明、调用 Native、解析 JSON 和展示返回栈。

1.2 库提供的能力

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

  • 目标页注册(register):navExtRegister 用 JSON 声明目标页(id、isTopLevel、deepLinks、参数表),返回上游 NavDestination.route 算法生成的路由模板——必选参数进路径段、可选/可空参数进 query,如 searchresult/{search}?category={category};
  • 类型化导航(navigate):navExtNavigate 携带类型化参数值,引擎先用上游 NavRoute 逻辑把值编码成路由串,再从路由串解码回类型值——一次 round-trip,Int 是数字、Boolean 是布尔、Float 是小数、可空无默认是 null;
  • NavOptions:singleTop(同页替换栈顶)与 popUpTo + inclusive(出栈到指定目标页);
  • 返回栈(navigateBack):出栈并在状态页可视化整条栈,根页面拒绝出栈并回显上游风格错误;
  • 深链(deepLink):navExtDeepLink 按 {name} 占位符匹配注册的 uriPattern,uri query 补充可选参数。

响应包含:

字段作用
ok操作是否成功,失败时 error 带原文(含上游异常原文,如 search not present in arguments for destination searchresult.)。
current当前栈顶条目:{id, route, isTopLevel, arguments},arguments 为类型化原生 JSON。
backStack整条返回栈,逐条回显 route 串。
destinations已注册目标页:路由模板、参数规格(name/type/nullable/hasDefault)、deepLinks。

引擎侧完整能力(模板生成、必选/可选分流、默认值应用、可空省略、五类型编解码)全部来自上游 NavDestination / NavRoute / NavArgs 的原版算法,随编译进入 .so;JVM 目标保留作对照编译,证明纯化后的模型在上游工具链语义下照常编译。

1.3 实现适配

维度要求
代码复用annotation 模块(@NavDestination / @NavArgument / NavArgumentType)100% 原样编译;runtime 路由模型 NavDestination / NavRoute 算法逐行保留,仅把 androidx.navigation 类型替换为纯 Kotlin 的 NavArgumentSpec(全仓唯一新增库源文件);Compose 四文件(NavHost / NavGraphBuilder / NavAction / NavArgs)100% 原样移入 composeMain,不删一行。
平台目标根构建直接声明 ohosArm64 + ohosX64,保留 jvm 作对照编译,库模块出 KLIB,example/nativeApp 出双 ABI 动态库。
构建隔离根构建替换为极简 ohos 构建(cmp_qrose 模式);KSP 编译器、wear 模块、sample-app*、build-logic、docs 留盘不进构建。
桥接稳定使用 5 个 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址;异常永远不跨 FFI。
依赖纪律上游本来就是零第三方依赖(除 androidx.navigation/compose),鸿蒙侧保持零外部依赖,桥的 JSON 手写自包含。
UI 完整四 Tab 覆盖全部桥接入口:目标页注册、类型化导航、深链、返回栈状态。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。NavHost / NavGraphBuilder 是 Compose 侧资产,已隔离保留;ArkTS Demo 只是把桥暴露的导航闭环画出来。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点模块与 Compose 依赖边界、确定交付物
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.0.0 钉版、Gradle 降 8.14.1
第 3 阶段:模型纯化       ── NavArgumentSpec 替换 androidx.navigation 类型、composeMain 隔离四个 Compose 文件
第 4 阶段:桥协议建模     ── 5 接口 JSON 契约、类型化载荷、错误通道
第 5 阶段:原生桥接       ── @CName C ABI、C++ N-API、参数校验和内存释放
第 6 阶段:ArkUI 宿主     ── 四 Tab 页面、HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
navigation-compose-extended/             上游路由库(进 ohos 构建,jvm + ohosArm64 + ohosX64)
  src/commonMain/                        纯化后的路由模型:NavDestination / NavRoute / NavArgumentKey / NavArgumentSpec / RouteConstants
  src/composeMain/                       上游 Compose 四文件原样隔离(不编译)
navigation-compose-extended-annotation/  注解词汇表(100% 原样,进 ohos 构建)
navigation-compose-extended-compiler/    KSP 编译器(jvm-only,留盘不进 settings)
navigation-compose-extended-wear/        Wear Compose 专用,留盘不进 settings
sample-app* / gradle/build-logic / docs  上游示例与文档站,留盘不进构建
example/nativeApp/                       Kotlin/Native 动态库(Bridge + Engine + Json)
example/harmonyApp/                      DevEco Stage、CMake N-API 和 ArkTS 页面
README.OpenHarmony.md / _CN.md           鸿蒙适配说明(中英双份)
README.upstream.md                       上游原文保存

构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :navigation-compose-extended、:navigation-compose-extended-annotation 与 :example:nativeApp,rootProject.name = "ohos_navigation-compose-extended"。上游的 AGP classpath、dokka / vanniktech 发布插件、spotless 约定插件(gradle/build-logic includeBuild)全部随根构建替换剥离——本 fork 不发 Maven Central。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformHarmonyOS Kotlin 2.2.21-1.0.0(eazytec Maven,社区批准钉法,qrose 同款)
JDK21(Adoptium,org.gradle.java.home 写进 gradle.properties——本机 java 不在 PATH,必须显式指定)
OpenHarmony 目标ohosArm64 + ohosX64(jvm 保留作对照编译)
Gradle8.14.1(上游 9.8.1 必须降级:2.2.21 旧线不支持 Gradle 9.x)
依赖零第三方依赖(上游除 androidx.navigation/compose 外本就零依赖,鸿蒙侧同样零依赖)
DevEco product5.1.0(18) 兼容
DevEco Studio5.1.0 Release
ABIarm64-v8a + x86_64
Bundleorg.terminator.ohos.navext
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 源码依赖度盘点
模块文件数依赖形态处置
navigation-compose-extended8NavArgumentKey / RouteConstants 纯 Kotlin;NavDestination / NavRoute 混用 androidx.navigation 类型;NavHost / NavGraphBuilder / NavAction / NavArgs 绑 Compose runtime 与 savedstate模型纯化后编译(jvm + 双 ohos),Compose 四文件隔离
navigation-compose-extended-annotation1纯 Kotlin 注解 + 枚举,零依赖整体编译(一个字节不改)
navigation-compose-extended-compiler13KSP symbol-processing-api + kotlinpoet,jvm-only留盘不进构建
navigation-compose-extended-wear2androidx.wear.compose留盘不进构建

耦合点具体到符号:NavDestination 用了 NamedNavArgument / NavArgumentBuilder / navArgument() / NavDeepLink.Builder;NavArgs 站在 NavBackStackEntry.arguments + androidx.savedstate.read 上;NavAction 持有 NavHostController 且 rememberNavAction 是 @Composable。前两者的字符串算法(模板拼接、参数分流、默认值回填)是纯逻辑,是库的内核价值;后两者是 Compose 胶水。切分线就画在这里。

1.4 页面边界
标题区          navext · OPENHARMONY(CMP 导航库 · Kotlin/Native ohosArm64/ohosX64)
Tab 切换        目标页 / 导航 / 深链 / 状态
目标页 Tab      已注册目标页卡片:id + isTopLevel 徽标、路由模板、参数表(name/type/nullable/hasDefault)、deepLinks;非法注册演示
导航 Tab        快速导航按钮(home/settings/yourlibrary)、NavOptions 开关(singleTop/popUpTo/inclusive)、searchresult 与 player 参数表单
深链 Tab        uri 输入框 + 预置深链按钮 + 不匹配报错演示
状态 Tab        当前条目(route + 类型化参数)、返回栈逐条、目标页数量
状态条          页面底部统一渲染 ok:false 错误原文(红色)

页面启动时先按序注册 5 个目标页(home 必须第一个——首注册即返回栈根),再 navExtState() 拉取整份状态。

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

2.1 制品线判断(本适配的路线分岔口)

先回答"能不能像 KStore 那样真编译整库":上游 commonMain 依赖 org.jetbrains.androidx.navigation:navigation-compose:2.9.2,社区 eazytec Maven 上的鸿蒙 CMP 线是 1.9.2 体系(qrose 适配时已验证 Compose 1.12 与之对不齐)。即使强行换 1.9.2 线制品,上游源码按 Compose 1.12 / Kotlin 2.4 API 写成,编译期就会塌方。结论:整库直编此路不通,退到 cmp_qrose 已审核的"内核移植 + UI 隔离"路线——切出纯 Kotlin 的路由模型层,Compose 部分原样保留待工具链对齐后接回。

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

本库零 kotlinx 依赖,没有 KStore 那种"版本跟着 coroutines 制品走"的约束,直接用社区批准的标准钉法:HarmonyOS Kotlin 2.2.21-1.0.0。代价是 Gradle 必须从上游 9.8.1 降到 8.14.1(2.2.21 旧线的硬要求,qrose / haze 先例),wrapper 三件套(gradlew / gradlew.bat / gradle-wrapper.jar + properties)整体替换。

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

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

rootProject.name = "ohos_navigation-compose-extended"

include(":navigation-compose-extended")
include(":navigation-compose-extended-annotation")
include(":example:nativeApp")

gradle/libs.versions.toml 收敛为一行版本 + 一个插件别名;gradle.properties 写入 org.gradle.java.home(JDK 21)、version=1.6.2-ohos.1 与 kotlin.native.ignoreDisabledTargets=true 等 Kotlin/Native 开关。

2.3 编译路由模型

两个库模块收敛为纯 KMP 构建:

// navigation-compose-extended-annotation/build.gradle.kts(原样编译,一个字节不改)
kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()
    ohosArm64()
    ohosX64()
}

// navigation-compose-extended/build.gradle.kts(依赖注解词汇表)
kotlin {
    applyDefaultHierarchyTemplate()
    jvmToolchain(21)
    jvm()
    ohosArm64()
    ohosX64()

    sourceSets {
        commonMain.dependencies {
            implementation(project(":navigation-compose-extended-annotation"))
        }
    }
}

实测:

./gradlew :navigation-compose-extended-annotation:compileKotlinJvm \
          :navigation-compose-extended:compileKotlinJvm                 # BUILD SUCCESSFUL
./gradlew :navigation-compose-extended-annotation:compileKotlinOhosArm64 \
          :navigation-compose-extended-annotation:compileKotlinOhosX64 \
          :navigation-compose-extended:compileKotlinOhosArm64 \
          :navigation-compose-extended:compileKotlinOhosX64             # BUILD SUCCESSFUL
2.4 Compose 源集隔离的零成本手法

NavHost.kt / NavGraphBuilder.kt / NavAction.kt / NavArgs.kt 四个文件 git mv 到 src/composeMain/,一行不改。原理:composeMain 源集是 Compose Multiplatform Gradle 插件挂进层级模板的,本 fork 不 apply Compose 插件,这个目录就不参与任何目标的编译——上游原件躺在树里,社区 compose.ui ohos 制品与 CMP 1.12 对齐后,把插件和依赖加回来即可整体接回。这与 qrose 隔离上游 QrCodePainter 的手法同源。

根构建同时全局禁用 ohos 测试任务(上游无 commonTest,此行为防御性配置):

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

第 3 阶段:模型纯化(技术难点)

3.1 唯一新增源文件:NavArgumentSpec

上游 NavDestination.argumentsMap 的值类型是 NavArgumentBuilder.() -> Unit(androidx.navigation 的 DSL lambda),arguments 属性再经 navArgument() 转成 NamedNavArgument。纯化就是把这个接缝换成纯 Kotlin 数据类——全仓唯一新增的库源文件:

// navigation-compose-extended/src/commonMain/.../NavArgumentSpec.kt
class NavArgumentSpec(
    val name: String,
    val type: NavArgumentType,          // 来自 annotation 模块,五类型枚举原样复用
    val isNullable: Boolean = false,
    val defaultValue: Any? = null,
) {
    val isDefaultValuePresent: Boolean get() = defaultValue != null
}
3.2 算法逐行保留的替换

NavDestination 与 NavRoute 的纯化遵循一条纪律:只换类型引用,不动算法。

  • NavDestination.argumentsMap 类型 Map<K, NavArgumentBuilder.() -> Unit> → Map<K, NavArgumentSpec>;arguments 属性从 map { navArgument(...) } 变为 argumentsMap.values.toList();
  • argumentsRoute 模板算法(必选参数 joinToString(prefix="/") 生成 {name} 路径段、可选参数生成 ?a={a}&b={b} query 骨架)逐行保留,仅取值点 it.argument.isDefaultValuePresent → it.isDefaultValuePresent;
  • NavRoute.appendRequiredParameters / appendOptionalParameters(必选缺值抛 IllegalArgumentException("$name not present in arguments ...")、非空参数传 null 抛 "Argument with key ... is not nullable."、缺省时回填 defaultValue)逐行保留;
  • NavArgs.kt 整文件随 Compose 隔离(它站在 NavBackStackEntry + savedstate 上);其类型化取值语义(getIntOrNull / getFloatOrDefault 等)在桥的 parseRoute 里按同款错误文案重实现;
  • 一处提权:NavRoute.route 从 internal 改为 public——上游只有同模块的 Compose 帮手消费它,NAPI 模块要跨模块读取,纯可见性调整、逻辑零改动。

上游语义的两个"怪癖"原样保留并在 Demo 里可见:无必选参数的目标页 navRoute() 生成 home/(尾斜杠,来自 appendRequiredParameters 的 ?: append(PARAM_SEPARATOR) 分支);String 参数值里的 / 不转义(与 androidx.navigation 路由匹配同一限制)。

3.3 深链模式与解码器

NavDeepLink.Builder().setUriPattern(...) 的 uriPattern 以原始字符串列表保留在 deepLinkUris。桥侧匹配算法:模式与 uri 都按 / 切分并滤空段(scheme://host 自然成为前两段),{name} 段捕获对应路径段,uri query 再补充可选参数。parseRoute 解码器把路由串拆回 id / 路径段 / query,按 NavArgumentSpec.type 做五类型 cast(toBooleanStrictOrNull / toIntOrNull / toLongOrNull / toFloatOrNull / 原文),失败时抛上游风格错误(Argument 'id' expects Int but route carried 'abc'.)。

第 4 阶段:桥协议建模

4.1 JSON 契约
dev.sergiobelda.navigation.compose.extended/**(上游路由模型,纯化后编译)
        ↓ NavDestination.route 模板 / NavRoute.route 编码 / parseRoute 解码
Engine.kt(注册表 + 返回栈 + NavOptions + 深链匹配)
        ↓
Bridge.kt(@CName × 5,扁平 JSON 进出)
        ↓ UTF-8 JSON
N-API navExtRegister / navExtNavigate / navExtNavigateBack / navExtDeepLink / navExtState
        ↓
ArkUI Index.ets(四 Tab)

请求与响应:

注册  : {"id":"searchresult","isTopLevel":false,
         "deepLinks":["sample://searchresult"],
         "arguments":[{"name":"search","type":"String","nullable":false,"defaultValue":null},
                      {"name":"category","type":"String","nullable":true,"defaultValue":"All"}]}
响应  : {"ok":true,"id":"searchresult","route":"searchresult/{search}?category={category}",...}

导航  : {"id":"player","arguments":{"id":7,"shuffle":true,"volume":0.8},
         "singleTop":false,"popUpTo":"home","inclusive":false}
响应  : {"ok":true,"current":{...},"backStack":[...],"destinations":[...]}
失败  : {"ok":false,"error":"..."}

两个不对称设计:注册请求的 defaultValue 是按 type 的原生 JSON(Int 给数字、String 给字符串、null 表示无默认);状态响应的 arguments 是解码后的类型化原生 JSON("shuffle":true、"volume":0.8、"position":null),ArkTS 侧 JSON.parse 一次拿到可直接渲染的值,不需要再按字符串猜类型。参数缺键与显式 null 语义不同:缺键走上游默认值回填,显式 null 走上游"可空则省略 query 参数"分支——桥把两种情况都原样交给上游算法裁决。

4.2 手写 Json.kt(为什么不引 kotlinx-serialization ohos 制品)

KStore 证明了社区 Maven 已有 serialization ohos 制品线,本桥仍然手写 JSON,理由有二:其一,上游库与桥都保持零第三方依赖,.so 体积与依赖面最小化;其二,qrose 审核先例就是自包含 MiniJson。但导航桥的载荷比 qrose 的扁平键值对复杂——参数表是数组、参数值是对象、深链模式是字符串数组——所以 Json.kt 是完整递归下降解析器(对象 / 数组 / 字符串 / 数字 / 布尔 / null、\uXXXX 转义、尾随字符校验)加一个支持嵌套输出的 JsonWriter,约 240 行。

4.3 错误通道

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

@CName("NavExtNavigate")
fun navExtNavigate(request: String): String = bridge {
    /* 解析 → 类型校验 → NavRoute 编码 → parseRoute 解码 → 入栈 */
}

private inline fun bridge(block: () -> String): String =
    try {
        block()
    } catch (t: Throwable) {
        JsonWriter().beginObject()
            .field("ok", false)
            .field("error", t.message ?: t.toString())
            .endObject().toString()
    }

非法 JSON、未知目标页、未知参数类型、必选参数缺值、类型不匹配、根页面出栈、深链不匹配,全部走同一个 ok:false 通道,错误文案优先透传上游原文,ArkTS 侧统一渲染到状态条。

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

5.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry(libnavext_napi.so)
  │ const char*
  ▼
Kotlin/Native C ABI(libohosnavext.so)
  │ NavExtRegister / NavExtNavigate / NavExtNavigateBack / NavExtDeepLink / NavExtState
  ▼
UTF-8 JSON + DisposeString

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

5.2 Kotlin/Native 导出函数
C ABI 符号ArkTS 方法覆盖内核
NavExtRegisternavExtRegister(req)目标页声明 → NavDestination.route 模板生成;首注册即返回栈根
NavExtNavigatenavExtNavigate(req)navRoute(...) 参数编码 + parseRoute 类型化解码 + singleTop / popUpTo / inclusive
NavExtNavigateBacknavExtNavigateBack()出栈;根页面抛上游风格错误
NavExtDeepLinknavExtDeepLink(req)uriPattern 占位符匹配 + query 补参 + 导航
NavExtStatenavExtState()整份状态(当前条目 / 返回栈 / 目标页表)
5.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"navExtRegister", nullptr, NapiNavExtRegister, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"navExtNavigate", nullptr, NapiNavExtNavigate, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"navExtNavigateBack", nullptr, NapiNavExtNavigateBack, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"navExtDeepLink", nullptr, NapiNavExtDeepLink, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
    {"navExtState", nullptr, NapiNavExtState, nullptr, nullptr, nullptr,
     static_cast<napi_property_attributes>(napi_default | napi_enumerable), nullptr},
};

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

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

add_library(navext_napi SHARED napi_init.cpp)
target_link_libraries(navext_napi PUBLIC
  libace_napi.z.so
  libhilog_ndk.z.so
  ohosnavext)

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

export const navExtRegister: (request: string) => string;
export const navExtNavigate: (request: string) => string;
export const navExtNavigateBack: () => string;
export const navExtDeepLink: (request: string) => string;
export const navExtState: () => string;

第 6 阶段:ArkUI 宿主

6.1 目标页注册(镜像上游 sample app)
private registerDestinations(): void {
  this.doRegister({ id: 'home', isTopLevel: true, deepLinks: [], arguments: [] });
  this.doRegister({
    id: 'searchresult', isTopLevel: false, deepLinks: ['sample://searchresult'],
    arguments: [
      { name: 'search', type: 'String', nullable: false, defaultValue: null },
      { name: 'category', type: 'String', nullable: true, defaultValue: 'All' }
    ]
  });
  this.doRegister({ id: 'settings', isTopLevel: true, deepLinks: [], arguments: [] });
  this.doRegister({ id: 'yourlibrary', isTopLevel: true, deepLinks: [], arguments: [] });
  this.doRegister({
    id: 'player', isTopLevel: false, deepLinks: ['sample://player/{id}'],
    arguments: [
      { name: 'id', type: 'Int', nullable: false, defaultValue: null },
      { name: 'shuffle', type: 'Boolean', nullable: true, defaultValue: false },
      { name: 'volume', type: 'Float', nullable: true, defaultValue: 0.5 },
      { name: 'position', type: 'Long', nullable: true, defaultValue: null }
    ]
  });
}

五个目标页与上游 sample-app 的 HomeNavDestination / SearchResultNavDestination / SettingsNavDestination / YourLibraryNavDestination 一一对应,player 是五类型参数的完整验收场。

6.2 调用封装与状态刷新
private runStateOp(raw: string): void {
  const checked: BridgeResult = this.callBridge(raw);   // JSON.parse + ok:false 进状态条
  if (checked.ok) {
    this.applyState(JSON.parse(raw) as StateResponse);  // current / backStack / destinations 全量刷新
  }
}

所有 NAPI 调用经 callBridge 统一出口,页面状态永远来自引擎返回值而不是本地推断——返回栈、类型化参数、模板字符串全部是 Native 侧真数据。

6.3 构建、签名和安装
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
# 产物经 copyKnSoArm64 / copyKnSoX64 自动回填 example/harmonyApp/entry/libs/{arm64-v8a,x86_64}

cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --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.navext -m entry

四、完整代码对照

4.1 整体架构

dev.sergiobelda.navigation.compose.extended/**(上游路由模型,纯化后编译)
    │ NavDestination.route 模板 / NavRoute.route 编码
dev.sergiobelda...extended.annotation/**(上游注解词汇表,原样编译)
    │ NavArgumentType 五类型枚举
Engine.kt + Json.kt + Bridge.kt
    │ NavExtRegister / Navigate / NavigateBack / DeepLink / State
libnavext_napi.so
    │ navExt* 五方法
Index.ets
    │ JSON parse + @State 更新
    ▼
ArkUI 真机页面(四 Tab)

4.2 文件清单

文件职责
settings.gradle.kts极简 ohos 构建、eazytec Maven 置前、ohos_navigation-compose-extended、compiler/wear/sample 移出
gradle/libs.versions.tomlKotlin 2.2.21-1.0.0 钉版(收敛为单版本单插件)
build.gradle.kts(根)版本 1.6.2-ohos.1、全局禁用 Ohos*Test* 任务
navigation-compose-extended/build.gradle.kts纯 KMP:jvm + ohosArm64 + ohosX64,依赖 annotation
navigation-compose-extended-annotation/build.gradle.kts纯 KMP 同构,源码零改动
.../src/commonMain/.../NavArgumentSpec.kt唯一新增库源文件(纯 Kotlin 参数规格)
.../src/commonMain/.../NavDestination.kt / NavRoute.kt类型替换纯化,算法逐行保留;route 提权 public
.../src/composeMain/(4 文件)上游 Compose 原件 100% rename 隔离
example/nativeApp/build.gradle.kts双 ABI sharedLib(baseName=ohosnavext)、cxxShared 定位 libc++、copyKnSo* 回填
nativeApp/.../Bridge.kt5 个 C ABI、类型校验、错误通道、状态序列化
nativeApp/.../Engine.kt注册表、返回栈、singleTop/popUpTo/inclusive、深链匹配、parseRoute 解码
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 四 Tab 验收页面
README.OpenHarmony_CN.md鸿蒙适配主文档(13 节)

4.3 关键 API 对照

层次上游 API桥接口作用
模型NavDestination.routenavExtRegister 响应 route必选路径段 + 可选 query 的模板生成
模型navRoute(vararg pairs).routenavExtNavigate参数值编码成路由串
模型NavArgs.getIntOrNull 等状态 arguments类型化解码(五类型 + null 语义)
ComposerememberNavAction(navController).navigate(navRoute)navExtNavigate导航动作
ComposenavController.navigateUp() / popBackStack()navExtNavigateBack出栈
androidxNavOptions(singleTop, popUpTo, inclusive)navExtNavigate 参数导航选项
androidxNavDeepLink uriPatternnavExtDeepLink深链匹配
KSP@NavDestination 注解 + 编译期生成ArkTS JSON 注册同一模型在鸿蒙侧的表达(编译器留盘)
NativeNavExt* × 5C ABI JSON 导出引擎入口
ArkTScallBridge / runStateOpnavExt* 五方法触发操作并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const raw: string = navExtNavigate(JSON.stringify({
  id: 'player',
  arguments: { id: 7, shuffle: true, volume: 0.8 },
  singleTop: false
}));
// res.ok === true;res.current = { id:'player', route:'player/7?shuffle=true&volume=0.8',
//   arguments:{ id:7, shuffle:true, volume:0.8, position:null } }——编码后解码,类型完整回来
val pairs = arguments.entries.map { (k, v) -> StringArgKey(k) to v }.toTypedArray()
val route = destination.navRoute(*pairs).route      // 上游 NavRoute 编码
Entry(destination, route, parseRoute(route, destination))  // 引擎解码回类型值

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

五、关键决策说明

决策 1:移植路由模型而不是等 CMP 对齐或重写导航

整库直编被依赖矩阵挡死(上游 navigation 2.9.2 / Compose 1.12.2,社区鸿蒙线 CMP 1.9.2),重写导航等于再造一个轮子。库的内核价值是模板生成与参数编解码的字符串算法——纯逻辑、零平台依赖、天然可移植。切出这一层原样编译,Compose UI 层隔离保留,是既守住"上游原件"又落地真机的唯一路径。

决策 2:composeMain 隔离而不是删除

NavHost / NavGraphBuilder / NavAction / NavArgs 四文件承载上游全部 Compose 资产。删除省事但断送回接可能;composeMain 是 Compose 插件挂载的源集,不 apply 插件即不参与编译——零构建配置实现"保留但不编译",社区工具链对齐后加回插件即可整体复活(qrose 审核同款手法)。

决策 3:Kotlin 2.2.21-1.0.0 + Gradle 8.14.1 降级钉版

与 KStore 升到 2.4.20 线相反:本库零 kotlinx 依赖,不存在"版本跟着制品走"的牵引,直接用社区批准的 2.2.21 标准钉法。代价是 Gradle 从上游 9.8.1 降到 8.14.1(旧线硬要求)。两条工具链线的经验不能互相套用——选线依据是依赖矩阵,不是版本新旧。

决策 4:KSP 编译器与 wear 模块留盘不进构建

编译器是 jvm-only 的 KSP 处理器,本身不需要 ohos 目标;但它生成的代码引用 navArgument / NavArgumentBuilder 等 androidx.navigation 运行时类型,ohos 侧无对应物。鸿蒙侧由 ArkTS JSON 注册表达同一模型,注解词汇表(NavArgumentType 五类型)经 annotation 模块原样共享,保证两侧语义同源。

决策 5:手写 Json.kt 保持零依赖

KStore 引入 serialization ohos 制品线是因为引擎本身依赖 serialization;本库与桥都零第三方依赖,引一套 kotlinx 制品只为解析 JSON 得不偿失。手写递归下降解析器约 240 行(qrose MiniJson 的嵌套加强版),.so 依赖面最小,社区审核有先例。

六、测试与验证

6.1 测试环境

本次验证使用 Windows(x64)、JDK 21(Adoptium jdk-21.0.12.101-hotspot)、DevEco Studio 5.1.0 Release、OpenHarmony 命令行工具(hvigor / hdc)与鸿蒙真机(arm64)。

6.2 Gradle 编译验证

./gradlew :navigation-compose-extended-annotation:compileKotlinJvm \
          :navigation-compose-extended:compileKotlinJvm
./gradlew :navigation-compose-extended-annotation:compileKotlinOhosArm64 \
          :navigation-compose-extended-annotation:compileKotlinOhosX64 \
          :navigation-compose-extended:compileKotlinOhosArm64 \
          :navigation-compose-extended:compileKotlinOhosX64

jvm 目标保留作对照:纯化后的模型(类型替换 + 一处提权)在上游工具链语义下照常编译,证明改动没有引入行为面变化。上游两库模块无 commonTest 测试源集,正确性闸门由真机 round-trip 用例承担(见 6.4)。

6.3 链接与符号验证

:navigation-compose-extended-annotation:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
:navigation-compose-extended:compileKotlinOhosArm64 / OhosX64: BUILD SUCCESSFUL
linkDebugSharedOhosArm64: BUILD SUCCESSFUL → arm64-v8a/libohosnavext.so(约 3.8MB debugShared)
linkDebugSharedOhosX64:  BUILD SUCCESSFUL → x86_64/libohosnavext.so(约 3.5MB debugShared)
copyKnSoArm64 / copyKnSoX64: 三件套回填(libohosnavext.so + libc++_shared.so)
生成的 libohosnavext_api.h: NavExtRegister / NavExtNavigate / NavExtNavigateBack /
                            NavExtDeepLink / NavExtState + libohosnavext_symbols 全部声明在列

HAP 侧:DevEco 打开 example/harmonyApp,CMake 编出 libnavext_napi.so(双 ABI),CompileArkTS → PackageHap → 签名 → hdc install → aa start 真机进入前台。

6.4 功能用例

  1. 启动后目标页 Tab 出现 5 张卡片,searchresult 的模板为 searchresult/{search}?category={category},player 的模板为 player/{id}?shuffle={shuffle}&volume={volume}&position={position}——来自 NavDestination.route 上游算法;
  2. home 的当前路由是 home/(尾斜杠,上游 NavRoute 无必选参数分支的原语义);
  3. 导航到 searchresult 只填 search=qrose,返回栈出现 searchresult/qrose?category=All——缺键参数被上游默认值回填;
  4. 再填 category=Music,路由变为 searchresult/qrose?category=Music;
  5. 导航到 player 填 id=7, shuffle=true, volume=0.8,状态页 arguments 显示 id:7(Int)、shuffle:true(Boolean)、volume:0.8(Float)、position:null——编码 → 解码 round-trip,五类型中四类型一次验证;
  6. search 留空导航,状态条红字 search not present in arguments for destination searchresult.——上游异常原文透传;
  7. 开 singleTop 连续导航同一目标页,栈顶替换、栈深不变;popUpTo home + inclusive 后整栈回到新页;
  8. 深链 sample://searchresult?search=qrose&category=Music 直接入栈到 searchresult;sample://player/7?volume=0.8 命中 {id} 占位符;sample://unknown 返回 No destination deep link matches 'sample://unknown'.;
  9. 非法注册(type 给 Double)返回 Unknown argument type 'Double'. Expected one of Boolean, Float, Int, Long, String.;
  10. 根页面点返回,状态条红字 Cannot navigate back from the root destination.。

6.5 验证结论

路由模型、注解词汇表、参数编解码、返回栈与 NavOptions 语义、深链匹配、Native 动态库、C ABI、N-API、ArkUI 页面和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,页面展示了真实的导航会话:模板生成、类型化 round-trip、singleTop/popUpTo、深链与返回栈可视化(见下节效果图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

7.2 命令速查

git clone https://atomgit.com/oh-tpc/navigation-compose-extended.git
cd navigation-compose-extended
export JAVA_HOME="/path/to/jdk-21"
./gradlew :navigation-compose-extended:compileKotlinOhosArm64 \
          :navigation-compose-extended-annotation:compileKotlinOhosArm64          # klib
./gradlew :example:nativeApp:linkDebugSharedOhosArm64 :example:nativeApp:linkDebugSharedOhosX64
cd example/harmonyApp
hvigorw assembleHap -p product=default -p buildMode=debug --no-daemon
hdc install -r entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b org.terminator.ohos.navext -m entry
hdc shell hilog | grep NavExt

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须让路由模型真正编译成 ohosArm64 / ohosX64 动态库;
  2. settings.gradle.kts 里 eazytec 双仓必须排在 pluginManagement 与 dependencyResolutionManagement 最前,否则配置期直接 Unresolved reference: ohosArm64;
  3. Kotlin 2.2.21-1.0.0 旧线不支持 Gradle 9.x:上游 wrapper 9.8.1 必须整体替换为 8.14.1(gradlew / gradlew.bat / wrapper jar + properties 四件一起换,只改 properties 会起不来);
  4. 上游 Gradle 9 的项目级 .gradle 缓存与 8.14.1 不兼容,切换工具链后先删项目 .gradle 再 sync;
  5. 纯化模型时的可见性陷阱:private 泛型实参(NavDestination<StringArgKey> 的 key 类)暴露给 internal 子类会编译报错,key 类必须 internal;跨模块读上游 internal val NavRoute.route 必须提权 public——只提可见性、不动逻辑;
  6. Kotlin 源码里手写字面量控制字符(如换页符)会在源文件中混入原始控制字节,工具链与编辑器都可能踩坑,用 12.toChar() 这类表达式规避;
  7. @CName 符号 ArkTS 不可直接 import——HarmonyOS CAdapter 不导出业务符号,必须 CMake NAPI 包装(import { navExtNavigate } from 'libnavext_napi.so',绝不是 libohosnavext.so);
  8. abiFilters 必须双 ABI(arm64-v8a + x86_64),否则真机或真机一侧报 does not provide an export named 'navExtNavigate';
  9. 每个 ABI 目录三件套(libnavext_napi.so / libohosnavext.so / libc++_shared.so)缺 KN so 安装直接报 9568347;
  10. hvigor 打 HAP 用 DevEco 自带 JBR,系统 JDK 8 会让 hvigorw assembleHap 半路报不相干的错;
  11. KN 返回字符串必须 DisposeString 归还运行时,N-API 层不能 free();
  12. 签名材料由 DevEco 写入 build-profile.json5,仓库内保持 signingConfigs 空数组,证书绝不进入 AtomGit。

8.2 已知问题

  • Compose 四文件(NavHost / NavGraphBuilder / NavAction / NavArgs)保留源码未编译,等社区 compose.ui / navigation-compose ohos 制品与上游 Compose 1.12 线对齐后接回,届时 ArkTS Demo 降级为 fallback;
  • KSP 编译器与 wear 模块留盘不进构建;鸿蒙侧目标页由 ArkTS JSON 注册表达,注解编译期生成的收益暂不可用;
  • 深链匹配为路径段 + {name} 占位符 + uri query,不支持 androidx NavDeepLink 的 .* 通配等完整 URI 语法;
  • String 参数值含 / 不转义(与 androidx.navigation 路由匹配同一限制,上游原语义);
  • 无 UI 层:导航状态由 ArkTS 渲染,真机 Demo 不含 Compose 画面;
  • Long 参数超过 2^53 时 ArkTS number 精度受限(JSON 层面数字类型的天花板);
  • android / ios 目标随根构建替换剥离(源文件留在树里),发布、dokka、spotless 一并剥离,本 fork 不发 Maven Central;
  • HAP 安装必须使用与 bundle name org.terminator.ohos.navext 匹配的签名。

8.3 未来优化方向

  • 社区 CMP 工具链对齐后把 composeMain 四文件接回编译,光栅化 Demo 升级为 Compose NavHost 完整形态;
  • 扩展 KSP 编译器生成 ohos 侧注册代码(注解 → ArkTS JSON / Kotlin 注册表),恢复编译期代码生成的类型安全收益;
  • 增加导航状态变化推送(N-API 线程安全回表),替代操作后读回;
  • 评估 .har 形态发布,让 ArkTS 工程直接依赖而不是走 so 三件套;
  • 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;
  • 补充真机(x86_64)与其他设备 ABI 的验收矩阵和自动化截图脚本。

九、总结

9.1 核心难点回顾

上游 navigation-compose-extended(路由模型 + Compose 四文件 + KSP 编译器)
    → 制品线判断:navigation 2.9.2 / Compose 1.12.2 无 ohos 变体,整库直编不通
    → 工具链钉版 Kotlin 2.2.21-1.0.0 + Gradle 8.14.1(根构建替换,cmp_qrose 模式)
    → 模型纯化:NavArgumentSpec 替换 androidx.navigation 类型,算法逐行保留
    → composeMain 隔离四个 Compose 文件(100% rename,零成本可回接)
    → ohosArm64 / ohosX64 KLIB + 唯一新增 NavArgumentSpec.kt
    → C ABI(@CName × 5)+ 类型化 JSON 载荷
    → C++ N-API(DisposeString 纪律)
    → ArkTS JSON 解析
    → ArkUI 真机四 Tab 验收(注册 / 类型化导航 / NavOptions / 深链 / 返回栈)

9.2 封装层次

  • 模型层:上游原版 NavDestination 模板生成、NavRoute 参数编码、NavArgumentKey 词汇表(纯化后原样编译);
  • 引擎层:注册表、返回栈、singleTop / popUpTo / inclusive、深链占位符匹配、parseRoute 类型化解码;
  • Native 层:生成双 ABI 动态库并通过 5 个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理页面状态、目标页注册、操作分发与错误文本;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 导航类 CMP 库的可移植内核是"路由模型"而不是 UI:先把模板生成、参数编解码、类型化解码切出来编进 ohos,Compose 胶水隔离保留——依赖矩阵决定切分线,切分线决定适配成本;
  2. 源集隔离是零成本保真手法:composeMain 不 apply Compose 插件就不参与编译,上游原件一行不改躺在树里,工具链对齐后加回插件即可整体复活,比删除或 fork 重写都安全;
  3. round-trip 是桥正确性的唯一可信验收:NavRoute 把参数编码进路由串、parseRoute 再从同一条串解码回类型值,上下游语义是否对齐在状态页一眼可见——上游默认值回填、可空省略、异常文案全部原样透传,"内核没漂"不需要解释。

9.4 适配成果

当前适配已完成:

  • navigation-compose-extended 1.6.2 路由模型的双 ABI Kotlin/Native 编译(annotation 模块零改动,runtime 算法逐行保留,唯一新增 NavArgumentSpec.kt,Compose 四文件 100% 隔离保留);
  • 5 个 C ABI 符号和 C++ N-API 包装层(生成头 libohosnavext_api.h 验证全部符号在列);
  • 双 ABI 动态库(arm64 约 3.8MB / x86_64 约 3.5MB debugShared)+ 三件套自动回填 Gradle 任务;
  • DevEco Stage ArkUI 四 Tab 验收页面(模板生成、类型化 round-trip、singleTop / popUpTo、深链、返回栈可视化、错误原文透传);
  • HAP 构建、签名、设备安装和真机验证(见第七节效果图);
  • AtomGit 项目文档、OpenHarmony README(中英双份)、上游原文保存与本地效果图。

适配过程 FAQ

以下问题来自 ohosArm64 目标接入、NAPI 桥接、HAP 构建与真机验收的实际流程,按出现频率排序。

Q1:DevEco Studio Sync 失败或 ohpm 依赖下载超时,怎么处理?

  • 确认 DevEco Studio 版本与工程 build-profile.json5 的 compatibleSdkVersion 一致;
  • 在 File > Settings > Build, Execution, Deployment > Ohpm 配置可用的 ohpm 仓库镜像或代理;
  • 删除 oh_modules、oh-package-lock.json5 与 .hvigor 缓存后重新 Sync;
  • 公司网络需要代理时,同时给 ohpm 与 hvigor 配置 http.proxy。

Q2:CMake 报 Missing .../libxxx.so 或找不到 Kotlin/Native 动态库?

  • 先在 example 目录执行 ./gradlew :nativeApp:linkDebugSharedOhosArm64(模拟器用 linkDebugSharedOhosX64);
  • 确认 libxxx.so 与 libc++_shared.so 已复制到 ohos/entry/libs/<abi>/;
  • 检查 entry/build-profile.json5 的 abiFilters 与设备 ABI 是否一致(真机 arm64-v8a,模拟器 x86_64);
  • 清理 entry/.cxx 与 entry/build 后重试。

Q3:ArkTS 报 Cannot find module 'libxxx_napi.so' 或调用返回 undefined?

  • 确认 CMake 的 add_library(<target> SHARED ...) 名称与 import ... from 'libxxx_napi.so' 完全一致;
  • 解包 HAP,确认 libs/<abi>/libxxx_napi.so 已打包;
  • 删除 entry/build、entry/.cxx 与 oh_modules 后重新构建;
  • 检查 module.json5 的 srcEntry 与页面路径是否正确。

Q4:真机安装报 signature verification failed 或 install failed?

  • 在 DevEco Studio 的 Signing Configs 配置证书、Profile 与密码;
  • bundleName 必须与签名 profile 中的 bundle name 一致;
  • 设备需加入 profile 的 deviceIds,调试证书有效期需覆盖当前时间;
  • 用 hdc list targets 确认设备在线,再执行 hdc install -r xxx-signed.hap。

Q5:应用启动白屏、闪退或 NAPI 调用崩溃?

  • 用 hdc hilog | grep -i "napi\|kotlin" 查看原生日志;
  • 确认 libc++_shared.so 与 Kotlin/Native 工具链版本匹配,且 ABI 与设备一致;
  • 检查 C ABI 返回的字符串是否由 Native 侧分配、ArkTS 侧复制后是否调用 XxxFree 释放;
  • 在 DevEco 的 Native 调试配置中附加 lldb,定位空指针与越界访问。

Q6:Gradle 报 ohosArm64 目标不存在或 Kotlin/Native 编译失败?

  • 使用鸿蒙发行版 Kotlin(如 2.2.21-1.0.0),并在 settings.gradle.kts 配置 eazytec 仓库;
  • Gradle 建议 8.x、JDK 21,版本不匹配时先统一工具链;
  • 确认 kotlin { ohosArm64() } 已应用,且 local.properties 的 SDK 路径有效;
  • 清理 ~/.gradle/caches 中对应版本的 Kotlin/Native 缓存后重试。

Q7:HAP 构建报 SDK 版本不匹配或 compatibleSdkVersion 错误?

  • 对齐 DevEco Studio、HarmonyOS SDK 与 build-profile.json5 中的 compatibleSdkVersion;
  • 升级 DevEco Studio 后同步升级 hvigor 与 SDK,避免 modelVersion 与 IDE 不匹配;
  • 命令行构建时设置 DEVECO_SDK_HOME 指向 DevEco 的 sdk 目录。

Q8:命令行 hvigor 构建失败,但 DevEco 里能构建?

  • 使用 DevEco 自带的 node 与 hvigor:
    "$DEVECO/Contents/tools/node/bin/node" "$DEVECO/Contents/tools/hvigor/bin/hvigorw.js" ...;
  • 设置 JAVA_HOME、NODE_HOME、DEVECO_SDK_HOME;
  • 加上 --no-daemon 排除守护进程缓存影响,并清理 entry/build 与 .hvigor。

参考文档

Logo

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

更多推荐