本文记录 PreCompose 接入 OpenHarmony 的完整过程,覆盖 KMP 公共导航库盘点、双构建隔离、纯 Kotlin 内核提取、ohosArm64 / ohosX64 目标、Kotlin/Native 动态库、C ABI/N-API 桥接、ArkUI 真机页面、HAP 构建、签名和真机验收。

PreCompose 是 Compose Multiplatform 生态中使用最广的导航 / ViewModel / Lifecycle 库之一。它的公开 API(NavHost、rememberNavigator、PreComposeApp)是 Compose 函数,但它的一半实现是不依赖 Compose 的纯 Kotlin 状态机:路由基数树、返回栈算法、生命周期状态机、ViewModel。本次适配让这同一份公共实现进入 OpenHarmony ARM64 / x86_64 运行时,ArkUI 页面作为真机验收宿主,Compose Multiplatform(CMP)应用可以通过同一个 KMP 依赖使用这些导航 API。

项目地址: AtomGit/oh-tpc/https://atomgit.com/oh-tpc/PreCompose

上游仓库: Tlaster/PreCompose

开发工具: 华为云码道

环境安装: 环境安装指南

一、背景

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

PreCompose 为 Compose Multiplatform 提供 Navigator、NavHost、BackStackEntry、ViewModel、Lifecycle 与路由解析能力。KMP 项目通常把这些状态逻辑放在 commonMain,由 Android、iOS、桌面、Web 或其他 Native 宿主共享。OpenHarmony 应用同样需要这套公共能力,但默认构建并不会生成 OpenHarmony 的 Kotlin/Native 变体。

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

障碍具体问题
目标缺失必须增加 ohosArm64() 才能生成 OpenHarmony KLIB。
插件线冲突上游 Kotlin 2.0.0 / Compose 1.6.10 与 HarmonyOS Kotlin 2.2.21 两条插件线不能共存于一个 Gradle 构建。
Compose 依赖上游 commonMain 32 个文件里 17 个 import androidx.compose.*,无法整体编译到 ohos。
语言边界不同ArkTS 不能直接持有 Kotlin 对象,需要 C ABI、C++ N-API 和 JSON。
状态语义返回栈、生命周期、ViewModel 都是有状态对象,需要会话管理与事件可观察。
交付链路复杂.so、CMake、N-API、HAP、签名和 hdc 都要单独验收。

因此,本项目把边界放在双构建隔离、纯 Kotlin 内核提取、C ABI/N-API 桥接和 ArkUI 验收层。导航语义仍由公共实现负责,ArkTS 只负责调用 Native、解析 JSON 和展示结果。

1.2 库提供的能力

示例提供四个可观察场景:

  • 路由匹配:chi 风格基数树解析静态段、{param} 参数、{id:[0-9]+} 正则、{param}? 可选段和 *path 通配符;
  • 返回栈导航:navigate(含 launchSingleTop、includePath、popUpTo 全部分支)、goBack、goBackWith(result)、goBack(popUpTo);
  • 生命周期:宿主 Active / InActive / Destroyed 状态机联动栈顶条目,Destroyed 清空返回栈;
  • ViewModel:StateHolder 复用同一实例、bagOfTags、clear() 释放 closeable 并回调 onCleared()。

NavSession 返回的每个返回栈条目 EntryInfo 包含:

字段作用
stateId条目唯一标识(uuid4 生成)。
route命中的路由模式,如 /detail/{id:[0-9]+}。
path完整导航路径。
pathMap路径参数键值对,如 {"id":"42"}。
query查询参数原文。
lifecycle条目当前生命周期状态(Initialized/Active/InActive/Destroyed)。

共享层还提供九项检查:静态路由命中、正则参数命中、正则拒绝(/detail/abc 不命中 [0-9]+)、可选参数省略(/edit)、通配符捕获、launchSingleTop 不叠加、popUpTo 弹栈范围、宿主 Destroyed 清栈、ViewModel 实例复用与 onCleared 顺序。

1.3 实现适配

维度要求
代码复用路由解析、返回栈、生命周期、ViewModel 逻辑全部位于上游 commonMain 的 1:1 提取层。
平台目标example/ 独立构建增加 ohosArm64 + ohosX64,生成双 ABI 动态库。
桥接稳定使用少量 C ABI 和 UTF-8 JSON,不传递 Kotlin 对象地址。
CMP 兼容CMP 应用复用同一 KMP 依赖;ArkUI 只负责真机验收。
UI 完整支持四 Tab 切换、导航操作、事件序列、错误边界和状态徽章。
仓库规范项目说明、文章、效果图和源码链接统一使用 AtomGit。

本项目的 ArkUI 页面不是 Compose Multiplatform UI 实现。CMP 工程可以直接依赖 OpenHarmony KMP 变体,在自己的 Compose 页面中使用标准导航 API。

二、实现路线图

第 1 阶段:项目初始化     ── 盘点源集、依赖分类和 OpenHarmony 交付物
第 2 阶段:目标与依赖打通 ── 双构建隔离、提取层和 focused build
第 3 阶段:导航场景建模   ── 建立路由表、栈快照、事件序列和九项检查
第 4 阶段:原生桥接       ── C ABI、C++ N-API、参数校验和内存释放
第 5 阶段:ArkUI 宿主     ── JSON 栈快照、操作按钮、状态徽章和错误边界
第 6 阶段:示例与验证     ── HAP、签名、设备安装、真机运行和效果图

三、逐步实现过程

第 1 阶段:项目初始化

1.1 工程边界
precompose/ … precompose-koin/   上游四个库模块(根构建,未改动)
example/settings.gradle.kts      ohos 独立 Gradle 构建(ohos_precompose)
example/nativeApp/               Kotlin/Native 动态库(提取层 + C ABI)
example/harmonyApp/              DevEco Stage、N-API 和 ArkTS 页面

example 是独立 Gradle 工程,不把 DevEco 工程作为根项目的 Kotlin 子模块,Gradle 和 Hvigor 可以分开执行;上游根构建(Kotlin 2.0.0 / Compose 1.6.10 / AGP 8.5.0)一行未改。

1.2 工具链矩阵
项目配置
Kotlin MultiplatformOpenHarmony focused build 使用 2.2.21-1.0.0(eazytec Maven)
JDK21
OpenHarmony 目标ohosArm64 + ohosX64
DevEco product6.0.1(21)
DevEco Studio6.0.1 Release
ABIarm64-v8a + x86_64
Bundlemoe.tlaster.precompose.ohos
export JAVA_HOME="/path/to/jdk-21"
java -version
1.3 页面边界
标题区          PRECOMPOSE / 导航能力工作台
能力区          返回栈 / 路由匹配 / 生命周期 / ViewModel
结果区          栈快照、条目徽章、路径参数、事件序列
操作区          导航 / 返回 / 带结果返回 / popUpTo 返回 / 重置
导航区          Tab 切换 + 快捷路由按钮

页面启动时通过 precomposeVersion() 读取版本信息,再发一次 {"op":"snapshot"} 同步当前返回栈。

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

2.1 双构建隔离与提取层

上游根构建与 HarmonyOS Kotlin 是两条不能共存的插件线,适配全部放在 example/ 独立构建里。对上游 commonMain 逐一统计 import 后发现:32 个文件中 15 个(47%)零 Compose 依赖,导航状态机整条链路(解析、入栈、出栈、生命周期流转、状态持有)都在纯 Kotlin 侧。这 15 个文件加上 4 个仅注解级依赖的文件被 1:1 拷入提取层,包名保持上游原样,可逐文件 diff:

kotlin {
    applyDefaultHierarchyTemplate()

    ohosArm64 {
        binaries.sharedLib {
            baseName = "ohosprecompose"
        }
    }

    ohosX64 {
        binaries.sharedLib {
            baseName = "ohosprecompose"
        }
    }
}

kotlinx-coroutines 与 com.benasher44:uuid 均无可解析的 ohos 制品,提取层因此做到零外部依赖:StateFlow 换轻量监听器,uuid4() 用 kotlin.random 生成,上游栈算法逐字保留。

2.2 构建核心库和 Native 产物
cd example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64

链接任务的 finalizedBy 会把产物自动拷入 HAP libs 目录:

example/harmonyApp/entry/libs/arm64-v8a/libohosprecompose.so + libc++_shared.so
example/harmonyApp/entry/libs/x86_64/libohosprecompose.so + libc++_shared.so

nm -gU libohosprecompose.so 可确认五个 PreCompose* 业务符号与 libohosprecompose_symbols 全部导出。

2.3 独立消费工程

DevEco 工程通过 CMake 以 IMPORTED 方式引用 KN 动态库,oh-package.json5 声明 NAPI 模块依赖:

{
  "dependencies": {
    "libprecompose_napi.so": "file:./src/main/cpp/types/libprecompose_napi"
  }
}

abiFilters 必须同时写 arm64-v8a 和 x86_64,否则 HAP 缺当前 ABI 的 so,安装直接报 9568347。

第 3 阶段:导航场景建模

3.1 JSON 契约
RouteParser / BackStackManager / Navigator(上游 1:1 提取)
        ↓ navigate / goBack / launchSingleTop / popUpTo
NavSession.snapshotJson()
        ↓ UTF-8 JSON
N-API precomposeNavigate()
        ↓
ArkUI Index.ets

页面不复制导航业务判断,每个栈快照和事件序列都由公共 Kotlin 代码生成。路由表前三个模式来自上游 todo sample 的真实用法:

listOf(
    "/home",                 // 静态
    "/detail/{id:[0-9]+}",   // 正则参数(上游详情页)
    "/edit/{id:[0-9]+}?",    // 可选参数(上游编辑页)
    "/settings/{section}",   // 普通参数
    "/help",                 // 多静态节点
    "/files/*path",          // 通配符
)
3.2 结果数据类
interface EntryInfo {
  stateId: string;                   // 条目唯一标识
  route: string;                     // 命中的路由模式
  path: string;                      // 完整路径
  pathMap: Record<string, string>;   // 路径参数
  query: string;                     // 查询参数原文
  lifecycle: string;                 // 条目生命周期状态
}
3.3 运行逻辑

launchSingleTop 命中时上游只做"去重后置顶"(提取层逐字保留):

if (options != null && options.launchSingleTop &&
    currentBackStacks.any { it.hasRoute(matchResult.route.route, path, options.includePath) }
) {
    currentBackStacks.firstOrNull { it.hasRoute(matchResult.route.route, path, options.includePath) }
        ?.let { entry ->
            updateStacks(backStacks.filter { it.stateId != entry.stateId } + entry)
        }
}

goBack 弹出并销毁栈顶(生命周期经 inActive → destroy 延迟销毁链流转):

fun pop(result: Any? = null) {
    val currentBackStacks = backStacks
    if (currentBackStacks.size > 1) {
        val last = currentBackStacks.last()
        updateStacks(currentBackStacks.dropLast(1))
        last.destroy()
    }
}

正则拒绝同样来自公共实现:/detail/abc 不命中 [0-9]+,返回 no route matches "/detail/abc"。

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

4.1 三层桥接
ArkTS
  │ JSON string
  ▼
C++ N-API entry
  │ const char*
  ▼
Kotlin/Native C ABI
  │ NavSession 状态机
  ▼
UTF-8 JSON + explicit free

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

4.2 Kotlin/Native 导出函数
@CName("PreComposeVersion")
fun preComposeVersion(): String

@CName("PreComposeRouteMatch")
fun preComposeRouteMatch(request: String): String

@CName("PreComposeNavigate")
fun preComposeNavigate(request: String): String

@CName("PreComposeLifecycle")
fun preComposeLifecycle(request: String): String

@CName("PreComposeViewModel")
fun preComposeViewModel(request: String): String

每个返回值都是 Kotlin/Native 分配的 C 字符串,整体 try/catch 兜底返回 {"ok":false,"error":"…"},错误文本保留上游 checkNotNull / require 原文。C++ 创建 ArkTS 字符串后立即调用 libohosprecompose_symbols()->DisposeString 归还 Kotlin 运行时内存——KN 字符串绝不能 free()。

4.3 C++ N-API 方法分发
napi_property_descriptor desc[] = {
    {"precomposeVersion", nullptr, NapiVersion, nullptr, nullptr, nullptr, napi_default, nullptr},
    {"precomposeRouteMatch", nullptr, NapiRouteMatch, nullptr, nullptr, nullptr, napi_default, nullptr},
    {"precomposeNavigate", nullptr, NapiNavigate, nullptr, nullptr, nullptr, napi_default, nullptr},
    {"precomposeLifecycle", nullptr, NapiLifecycle, nullptr, nullptr, nullptr, napi_default, nullptr},
    {"precomposeViewModel", nullptr, NapiViewModel, nullptr, nullptr, nullptr, napi_default, nullptr},
};

每个方法检查下标和参数类型是否为字符串。N-API 只负责参数类型、字符串创建和释放,不实现导航语义。

4.4 CMake 和类型声明
add_library(ohosprecompose SHARED IMPORTED)
set_target_properties(ohosprecompose PROPERTIES
  IMPORTED_LOCATION
    "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${OHOS_ARCH}/libohosprecompose.so")

add_library(precompose_napi SHARED napi_init.cpp)
target_link_libraries(precompose_napi PRIVATE
  libace_napi.z.so libhilog_ndk.z.so ohosprecompose)
export const precomposeVersion: () => string;
export const precomposeRouteMatch: (request: string) => string;
export const precomposeNavigate: (request: string) => string;
export const precomposeLifecycle: (request: string) => string;
export const precomposeViewModel: (request: string) => string;

第 5 阶段:ArkUI 宿主

5.1 加载版本和栈快照
import { precomposeVersion, precomposeNavigate } from 'libprecompose_napi.so';

const info = JSON.parse(precomposeVersion()) as VersionInfo;
const nav = JSON.parse(precomposeNavigate('{"op":"snapshot"}')) as NavInfo;
this.entries = nav.entries ?? [];

运行导航操作:

this.refreshNav('{"op":"navigate","path":"/detail/42"}');
this.refreshNav('{"op":"goBack"}');
5.2 页面交互

页面支持返回栈、路由匹配、生命周期和 ViewModel 四个 Tab,另有导航输入、快捷路由按钮、launchSingleTop 复选、popUpTo 输入和重置。所有栈快照与事件序列来自共享 Kotlin 结果,ArkTS 不重新实现导航语义。

5.3 对比度和错误边界

主操作使用深蓝色底和白字,栈顶条目浅蓝高亮,生命周期徽章四色区分(Active 绿 / InActive 橙 / Destroyed 红 / Initialized 灰),正文和辅助文字使用更深的灰色。Native 异常以 ok:false 形式显示在页面错误卡片,例如导航未注册路径会展示上游错误原文。

第 6 阶段:示例与验证

6.1 工程结构
example/
├── settings.gradle.kts              ohos 独立构建(eazytec Maven 置前)
├── nativeApp/src/ohosMain/kotlin/
│   ├── moe/tlaster/precompose/…     上游 19 个文件的 1:1 提取层
│   └── example/nativeapp/Bridge.kt  C ABI 五导出 + MiniJson
└── harmonyApp/
    ├── entry/src/main/cpp/CMakeLists.txt
    ├── entry/src/main/cpp/napi_init.cpp
    └── entry/src/main/ets/pages/Index.ets
6.2 构建、签名和安装
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64

# DevEco Studio 打开 example/harmonyApp,配置 Signing Configs 后:
hvigorw assembleHap --mode module -p product=default -p buildMode=debug

hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b moe.tlaster.precompose.ohos -m entry

四、完整代码对照

4.1 整体架构

RouteParser / BackStackManager / Navigator / LifecycleRegistry / ViewModel
    │ navigate / goBack / launchSingleTop / popUpTo
    ▼
Bridge.kt
    │ PreComposeVersion / PreComposeRouteMatch / PreComposeNavigate
    │ PreComposeLifecycle / PreComposeViewModel
    ▼
libprecompose_napi.so
    │ precomposeVersion / precomposeRouteMatch / precomposeNavigate
    │ precomposeLifecycle / precomposeViewModel
    ▼
Index.ets
    │ JSON parse + state update
    ▼
ArkUI 真机页面

4.2 文件清单

文件职责
example/settings.gradle.ktsohos 独立构建与 eazytec Maven 仓库
example/nativeApp/build.gradle.ktsohosArm64/ohosX64 动态库与产物拷贝任务
example/nativeApp/…/RouteParser.kt上游路由基数树 1:1 提取
example/nativeApp/…/BackStackManager.kt上游返回栈算法 1:1 提取
example/nativeApp/…/LifecycleRegistry.kt上游生命周期状态机 1:1 提取
example/nativeApp/…/viewmodel/ViewModel.kt上游 ViewModel 逐字提取(零 Compose 依赖)
example/nativeApp/…/Bridge.ktC ABI、JSON 返回和内存释放约定
example/harmonyApp/…/napi_init.cppN-API 导出和参数检查
example/harmonyApp/…/Index.etsArkUI 四 Tab 运行页面
README.OpenHarmony_CN.md适配文档、提取层对照表和验收记录

4.3 关键 API 对照

层次API作用
KMPRouteParser.find基数树匹配路径并产出 pathMap
KMPBackStackManager.push入栈,含 launchSingleTop / popUpTo 全分支
KMPNavigator.goBack弹栈并按生命周期链销毁条目
KMPLifecycleRegistry.updateState状态机推进与观察者分发
KMPViewModel.clear释放全部 closeable 并回调 onCleared
NativePreComposeRouteMatch返回路由匹配 JSON
NativePreComposeNavigate执行导航并返回栈快照 JSON
NativePreComposeLifecycle驱动宿主状态机
NativePreComposeViewModel驱动 ViewModel 行为并返回轨迹
N-APIprecomposeNavigate / …向 ArkTS 暴露五个入口
ArkTSrunNavigate / runMatch触发示例并刷新 UI

4.4 ArkTS 与 Kotlin 的边界

const nav = JSON.parse(precomposeNavigate('{"op":"navigate","path":"/detail/42"}')) as NavInfo;
@CName("PreComposeNavigate")
fun preComposeNavigate(request: String): String = try { … "{\"ok\":true,${session.snapshotJson()}}" }

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

五、关键决策说明

决策 1:双构建隔离,根构建零改动

ohosArm64() DSL 只存在于 HarmonyOS Kotlin 发行版;换掉根构建 Kotlin 会让 Compose Compiler、CMP 1.6.10 与 Android/iOS/JS 目标全部断链。独立 example/ 构建让 Android / iOS / Desktop / Web 用户照常消费上游制品,鸿蒙适配与上游永久可 diff。

决策 2:提取纯 Kotlin 内核而不是整体编译

量化盘点后确认 47% 的 commonMain 零 Compose 依赖,导航状态机整条链都在其中。提取副本与上游同包名同文件名,"没有漂"成为可以用 diff 复核的事实。

决策 3:零外部依赖

kotlinx-coroutines 与 uuid 都没有可解析的 ohos 制品。StateFlow 换监听器、uuid4 换 kotlin.random,把制品可达性从适配的关键路径上摘掉,上游算法本身不动。

决策 4:JSON 作为跨语言数据契约

JSON 让 ArkTS、C++ 和 Kotlin/Native 的边界清楚可调试,并允许后续增加字段而不暴露内部对象布局。

决策 5:ArkUI 作为验收宿主,CMP 复用 KMP 状态机

当前页面验证 OpenHarmony Native 链路,CMP 应用则直接依赖相同 KMP 依赖,在自己的 Compose UI 中使用 Navigator 状态机。

六、测试与验证

6.1 测试环境

本次真机验证使用 macOS(ARM64)、JDK 21、DevEco Studio、OpenHarmony 6.0.1(API 21) 命令行 SDK 和 HarmonyOS 真机(arm64)。

6.2 共享样例测试

上游 precompose/src/commonTest 的 12 个测试文件(覆盖 RouteParser、QueryString、BackStackManager、LifecycleRegistry)可在根构建执行 ./gradlew :precompose:jvmTest 复跑;提取层与上游同包名同文件名,标注"无差异"的文件可用 diff 逐字节复核。

6.3 HAP 验证

CompileArkTS: finished
PackageHap: finished
SignHap: finished(DevEco 配置签名后)
BUILD SUCCESSFUL
install bundle successfully
start ability successfully

命令行先完成双 ABI 打包(两 ABI × 三份 so 齐全),签名后在真机安装并进入前台。

6.4 功能用例

  1. 启动后自动读取版本并同步 /home 初始栈;
  2. 切换返回栈、路由匹配、生命周期、ViewModel 四个 Tab;
  3. 导航 /detail/42 后栈快照、路径参数徽章立即更新;
  4. 勾选 launchSingleTop 后重复导航 /detail/42 不叠加,/detail/43 仍叠加;
  5. popUpTo 指定 /home 弹回指定路由;
  6. 导航未注册路径显示上游错误原文;
  7. 宿主 Destroyed 清空返回栈且不再接受状态更新;
  8. ViewModel 重复创建返回同一实例,clear() 后事件序列显示 closeable 先释放、onCleared 后触发。

6.5 验证结论

Native 动态库、C ABI、N-API、ArkUI 页面和签名 HAP 已完成链路验证。应用在 HarmonyOS 真机上成功安装并进入前台,页面展示了真实返回栈快照、路径参数、生命周期徽章和事件序列(见下节截图)。

七、运行效果

7.1 真机截图

在这里插入图片描述

截图中可以看到:

  • 页面标题为「PreCompose · OpenHarmony」,副标题标注 CMP 导航/ViewModel 内核与 Kotlin/Native 双目标;
  • 版本行显示 1.6.1-ohos.1 · 上游 1.6.1 · Kotlin 2.2.21-1.0.0,证明页面正在调用 Native;
  • 顶部能力 Tab 为「返回栈 / 路由匹配 / 生命周期 / ViewModel」,当前选中返回栈;
  • 操作区提供导航输入框、快捷路由按钮、launchSingleTop 复选、popUpTo 输入与导航、返回、带结果返回、popUpTo 返回、重置按钮;
  • 返回栈显示 /home 条目,栈顶「▶ 当前」高亮,绿色 Active 徽章表示条目生命周期已激活;
  • 「栈变更事件」区域展示来自 Native 的真实事件序列(host observer 与 stack 变更日志);
  • 手机状态栏(实况胶囊、双卡、电量)表明这是真机运行而非模拟器。

7.2 命令速查

git clone https://atomgit.com/oh-tpc/PreCompose.git
cd PreCompose
export JAVA_HOME="/path/to/jdk-21"
cd example
./gradlew :nativeApp:linkDebugSharedOhosArm64 :nativeApp:linkDebugSharedOhosX64
# DevEco Studio 打开 example/harmonyApp,配置 Signing Configs 后:
hvigorw assembleHap --mode module -p product=default -p buildMode=debug
hdc install entry/build/default/outputs/default/entry-default-signed.hap
hdc shell aa start -a EntryAbility -b moe.tlaster.precompose.ohos -m entry

八、遗留问题与改进方向

8.1 踩坑复盘

  1. 只改 ArkTS 页面不算 KMP 适配,必须真正编译 ohosArm64 动态库;
  2. 官方 Kotlin 无 ohosArm64,必须用 eazytec Maven 的 HarmonyOS Kotlin 2.2.21-1.0.0,仓库置于 pluginManagement 首位;
  3. 根构建与 HarmonyOS Kotlin 插件线不能共存,双构建隔离是解法;
  4. N-API 不负责业务判断,C++ 只做类型检查、字符串转换和释放;
  5. Native 返回值交给 N-API 后必须调用 DisposeString 归还 Kotlin 运行时;
  6. 运行结果要来自真实 Native 调用,而不是页面静态文字;
  7. 签名工程必须与源码分离,证书不能进入 AtomGit。

8.2 已知问题

  • 鸿蒙侧没有 NavHost / rememberNavigator 等 Compose API,UI 层由 ArkTS 承担;
  • StateFlow 换成了监听器、uuid 换成了 kotlin.random,navigateForResult(suspend)未过桥;
  • SavedStateHolder 是缩减版,不做进程死亡恢复;
  • SceneRoute 的 deepLinks、NavTransition、SwipeProperties 未过桥;
  • 提取层是副本,上游升级时需按对照表人工同步。

8.3 未来优化方向

  • 社区出现与 Compose 1.6+ 对齐的 ohos 制品后,删除副本改用 srcDir 直接引用上游 commonMain;
  • coroutines ohos 制品可达后恢复 StateFlow 与 suspend 导航 API;
  • 增加 deepLinks、转场动画配置等更多导航场景的桥接验收;
  • 在 CI 中加入 ohos 链接检查、ELF 符号核查和 HAP 构建;
  • 补充不同设备 ABI、API 版本的验收矩阵。

九、总结

9.1 核心难点回顾

PreCompose commonMain 纯 Kotlin 内核
    → 双构建隔离(根构建零改动)
    → 1:1 提取层(19 个上游文件)
    → ohosArm64/ohosX64 Kotlin/Native
    → C ABI
    → C++ N-API
    → ArkTS JSON 解析
    → ArkUI 真机页面

9.2 封装层次

  • KMP 层:提供路由基数树、返回栈算法、生命周期状态机、ViewModel;
  • Native 层:生成双 ABI 动态库并通过五个 C ABI 输出 JSON;
  • N-API 层:完成参数检查、字符串转换和 Native 内存释放;
  • ArkTS 层:管理栈快照、事件日志、错误文本和页面交互;
  • DevEco 层:完成 CMake、HAP、签名、安装和真机运行。

9.3 三条经验

  1. 先量化"哪些文件真的依赖 Compose"再动手,47% 纯 Kotlin 这个数字直接决定了整条路线的可行性;
  2. 用 JSON 和少量 C ABI 代替跨语言对象传递,并明确字符串释放责任;
  3. 提取副本与上游同包名同文件名、逐文件标注差异,让"没有漂"可复核而不是一句承诺。

9.4 适配成果

当前适配已完成:

  • 上游导航内核 19 个文件的 ohos 提取层(15 个逐字 + 4 个标注差异);
  • ohosArm64 + ohosX64 双 ABI 动态库与五个 C ABI 符号导出;
  • C++ N-API 包装层与 ArkTS 四 Tab 验收页面;
  • 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

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐