开源鸿蒙平台 CMP 三方库「multiplatform-compose」适配全流程
本文记录
multiplatform-compose接入 OpenHarmony 的完整过程,覆盖上游定位盘点、社区 Compose-for-OHOS 制品线选型、2022→2026 工具链断代下的 Demo 源码移植(Material 2 → Material 3)、ohosArm64/ohosX64目标、Kotlin/Native 动态库、CMake / N-API 桥接、ArkTS 宿主、HAP 构建和真机验收。
multiplatform-compose(cl3m/multiplatform-compose,compose-jb 分支)是 2021 年的 Compose Multiplatform 演示 App:同一份 Compose UI 跑在 Android 与 iOS 上——Scaffold + 抽屉导航 + 10 个 Material 演示页(按钮、文本样式、LazyColumn、布局、计数器、平台问候、图片、输入框、底部导航、长文滚动),并用 PreCompose 做导航、Ktor 拉网络图片。本次适配补上第三端:让这同一份 Compose UI 直接跑在 OpenHarmony 上。与社区早期路线(qrose / haze 的"零依赖内核提取"、KStore 的"纯 Kotlin 库真编译")有本质区别:这个项目没有可提取的非 UI 内核——全部 26 个 Kotlin 文件里 100% 是 Compose UI,因此走路线 C"全 Compose UI"(lazytable / Reorderable / Haze 同款):社区 Compose-for-OHOS 分支1.9.2-1.1.0-13(skia 自绘后端)把真实的 Compose 场景树渲染进 ArkUI 宿主,界面上看到的一切都来自 Kotlin Compose 代码,不是 ArkTS 重写。
项目地址: AtomGit/oh-tpc/multiplatform-compose.git
上游仓库: cl3m/multiplatform-compose
开发工具: 华为云码道
环境安装: 环境安装指南
一、背景
1.1 为什么做 OpenHarmony 平台 KMP/CMP 适配
multiplatform-compose 的价值不在某个算法,而在"一份 Compose UI,多端运行"这个演示语义本身:上游用 JetBrains Compose Multiplatform 在 Android 与 iOS 上渲染同一份 commonMain UI,证明跨端 UI 可以不写两遍。OpenHarmony 应用同样需要这个证明——鸿蒙侧能不能不重写、直接复用这份 Compose 场景?但上游钉死在 2022 年的工具链(Kotlin 1.7.20 / Compose 1.2.1 / AGP 7.3.1 / jcenter 时代),与鸿蒙 Kotlin 2.2.21 线差了三个大版本,无法原样混编。
如果只把演示页面改成 ArkTS,页面可以显示按钮,却无法证明 Compose 编译器、场景树、skia 渲染管线在 OpenHarmony ARM64 设备上完成编译、链接和执行。适配需要同时验证:Compose 源码在 ohos 目标上的编译、社区 CMP 分支的 klib 解析、Native 动态库导出、ArkUI 宿主装载、生命周期桥接和 HAP 交付链路。
| 障碍 | 具体问题 |
|---|---|
| 目标缺失 | 必须增加 ohosArm64() / ohosX64() 才能生成 OpenHarmony 动态库。 |
| 工具链断代 | 上游 Kotlin 1.7.20 / Compose 1.2.1 / AGP 7.3.1 与鸿蒙工具链无法共存于一个 Gradle 构建;buildSrc/ 会被 Gradle 无条件自动构建,直接污染鸿蒙构建。 |
| 依赖矩阵 | Material 2、moe.tlaster:precompose(导航)、io.ktor(网络图)、material-icons-core 四件套全部没有 ohos 制品。 |
| 渲染边界 | Compose 场景树不能直接进 ArkUI,需要社区 CMP 分支 + skia 自绘 + ArkUIViewController 桥。 |
| 资源管线 | imageResource(id) 的资源管线在 ohos 线缺失,本地位图要走 cinterop / Canvas 替代。 |
| 交付链路复杂 | .so、CMake、N-API、HAP、签名和 hdc 都要单独验收。 |
因此,本项目把边界放在:Demo 源码的 ohos 移植(升 API 不改语义)、根构建替换(cmp_qrose 模式)、libkn.so 的导出与 ArkTS 宿主桥接。UI 语义完全由 Compose 负责,ArkTS 只负责装载 controller、转发生命周期。
1.2 库提供的能力
Demo 是一个真实的 Compose Multiplatform 应用会话,抽屉导航 + 10 个演示页全部由 Compose 渲染:
| Demo | 验收点 |
|---|---|
| HelloPlatform | 居中显示 Hello, OpenHarmony (Kotlin/Native)!(证明 Platform actual 生效) |
| Lorem | 长文可滚动(verticalScroll),滚动流畅无花屏 |
| LazyColumn | 列表懒加载 25 行 + 分隔线,快速滑动不丢帧 |
| Counter | + / - 按钮改数字,状态在离开 / 返回页面后保留(CompositionLocal) |
| Layout | 彩色 Box / Column / Row / Spacer 按上游配色排布(绿 / 品红 / 黄 / 红 / 黑 / 蓝) |
| TextStyles | 蓝 / 斜体 / 加粗 / 25sp / 手写体 / 等宽 / 衬线 / 30sp / 红各一行,随暗色模式变色 |
| Images | 上:200×300 橙色"笑脸"位图(Canvas 绘制);下:黑色占位框说明网络图待 Ktor ohos 引擎 |
| Buttons | 6 种按钮:Simple / 带边框 / 圆角 / Outlined / Text / Big,全部可点 |
| TextFields | 输入框可弹软键盘、实时回显 Text is ...、label / placeholder 正常 |
| BottomNavigation | 底部三 Tab(⌂ Tab1/2/3)切换 HelloPlatform / Layout / Counter,选中态高亮 |
壳层提供 Scaffold + 抽屉导航(左上 ☰ 呼出)、橙色 Material 主题(亮 / 暗双 scheme)、暗色模式与安全区的 CompositionLocal 语义保留。这十个页面就是"Compose 能不能在鸿蒙上真跑"的完整验收面。
1.3 实现适配
| 维度 | 要求 |
|---|---|
| 代码复用 | 上游 :test 共享库的 16 个源文件移植进 example/composeApp,升级到 Compose 1.9 / Material 3 API,逻辑零改动;仅 import 与废弃 API 替换。 |
| 平台目标 | example/composeApp 声明 ohosArm64 + ohosX64,binaries.sharedLib 产物 libkn.so + libkn_api.h,export 导出 compose-export 符号。 |
| 构建隔离 | 根构建替换为极简 ohos 构建(cmp_qrose 模式);上游 androidApp/、iosApp/、test/ 留盘不进 settings.gradle.kts;buildSrc/ 删除(版本目录由 toml 接管)。 |
| 渲染稳定 | Compose 场景经社区 CMP 分支 + skiko-ohos 自绘进 ArkUI;controller 接管 onPageShow / onPageHide / onBackPressed 生命周期。 |
| 依赖纪律 | 无 ohos 制品的依赖(M2 / precompose / ktor / icons)全部降级为等价 M3 或占位实现,不引入伪造网络栈。 |
| UI 完整 | 抽屉 + 10 个演示页全量保留,橙色主题与上游截图语义一致。 |
| 仓库规范 | 项目说明、文章、效果图和源码链接统一使用 AtomGit。 |
本项目的 ArkTS 壳不是 UI 实现。除宿主壳(EntryAbility / Index.ets,社区模板)外,界面上看到的一切都来自 Kotlin Compose 代码;ArkTS 侧一行业务 UI 都没有。
二、实现路线图
第 1 阶段:项目初始化 ── 盘点模块与依赖边界、查重定位(无可提取内核)、确定交付物 = HAP + 双 ABI libkn.so
第 2 阶段:目标与依赖打通 ── 根构建替换、eazytec Maven 置前、Kotlin 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13 钉版
第 3 阶段:源码移植 ── 16 个 commonMain 文件升级到 1.9/M3 API,迁入 example/composeApp;M2→M3、导航状态化、Ktor 降级
第 4 阶段:ohos 目标打通 ── ohosArm64+ohosX64 sharedLib + export 导出 + resource cinterop + publish Copy 任务
第 5 阶段:ArkTS 宿主 ── hvigor 工程骨架 + compose.har + CMake NAPI 胶水 + Compose({controller, libraryName})
第 6 阶段:验证交付 ── 双 ABI link 通过 → so 落位 → DevEco 打 HAP → 自动签名 → 真机验收(10 个 Demo 逐项过)
三、逐步实现过程
第 1 阶段:项目初始化
1.1 工程边界
multiplatform-compose/ rootProject.name = ohos_multiplatform_compose
├── settings.gradle.kts eazytec 双仓置前;只 include :example:composeApp
├── build.gradle.kts 根构建:三插件 apply false + group/version + 全局禁用 ohos 测试任务
├── gradle.properties JDK21 / version=0.0.1-ohos.1 / kotlin.native.* / rendererBackend=skia
├── gradle/libs.versions.toml kotlin=2.2.21-1.1.0-05,compose=1.9.2-1.1.0-13,三插件别名
├── example/
│ ├── composeApp/ ★ 鸿蒙 Demo(本 fork 的主体)
│ │ ├── build.gradle.kts sharedLib baseName="kn" + export + resource cinterop + publish 任务
│ │ └── src/
│ │ ├── commonMain/kotlin/com/rouge41/kmm/compose/
│ │ │ ├── Platform.kt / Resources.kt expect(沿上游包名)
│ │ │ └── test/ App/Content/Theme + 10 个 Demo(M3 化)
│ │ └── ohosMain/
│ │ ├── kotlin/.../Platform.kt / Resources.kt actual 实现
│ │ ├── kotlin/.../test/MainArkUIViewController.kt @CName 场景入口
│ │ └── cinterop/ OH_ResourceManager 绑定(rawfile)
│ └── harmonyApp/ ★ DevEco hvigor HAP 工程
│ ├── AppScope/app.json5 bundle=org.terminator.ohos.multiplatformcompose, 0.0.1-ohos.1
│ ├── build-profile.json5 compatibleSdkVersion 5.0.5(17),signingConfigs 留空
│ └── entry/
│ ├── libs/compose.har 社区 @cpf-kmp-cmp/compose 1.9.2-0.3.0(进 git)
│ ├── oh-package.json5 依赖 compose.har + libentry.so 类型
│ └── src/main/
│ ├── cpp/CMakeLists.txt IMPORTED libkn.so + find_package(compose) + skikobridge
│ ├── cpp/napi_init.cpp androidx_compose_ui_arkui_init + MainArkUIViewController 注册
│ ├── ets/entryability/ 标准 UIAbility
│ ├── ets/pages/Index.ets Compose({ controller, libraryName: 'entry' })
│ └── resources/ 图标 / 字符串 / main_pages
├── androidApp/ iosApp/ test/ 上游遗留(Android/iOS 壳与共享库),留盘不进构建
├── README.md / README.upstream.md 鸿蒙中文首页 / 上游原文备份
└── README.OpenHarmony.md / _CN.md 英文摘要 / 鸿蒙适配主文档(19 节)
职责边界一句话:Gradle 负责 Kotlin → libkn.so,hvigor 负责 so + ArkTS → HAP;两者唯一的连接点是 publishDebugBinariesToHarmonyApp 这个 Copy 任务。
构建拓扑走 cmp_qrose 模式(根构建即 ohos 构建):settings.gradle.kts 只 include :example:composeApp,rootProject.name = "ohos_multiplatform_compose"。上游的 buildSrc/(老版本目录)整个删除——Gradle 会对 buildSrc 无条件自动构建,其内容只服务 2022 年的 Android 构建,留着必然污染鸿蒙构建;版本管理由 gradle/libs.versions.toml 接管。
1.2 工具链矩阵
| 项目 | 配置 |
|---|---|
| Kotlin Multiplatform | HarmonyOS Kotlin 2.2.21-1.1.0-05(eazytec Maven) |
| Compose Multiplatform | 1.9.2-1.1.0-13(社区 OH 分支,skia 自绘后端) |
| skiko | 0.9.22.2-1.1.0-13(ohos 线,rendererBackend=skia) |
| JDK | 21(org.gradle.java.home 指向本机 JDK 21) |
| OpenHarmony 目标 | ohosArm64 + ohosX64(sharedLib,baseName=kn) |
| Gradle | 8.14.1(wrapper 升级;2.2.21 线经验值) |
| DevEco product | 5.0.5(17) 兼容,modelVersion 6.0.0 |
| DevEco Studio | 6.0.0 Release |
| ABI | arm64-v8a + x86_64 |
| Bundle | org.terminator.ohos.multiplatformcompose |
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
java -version
gradlew 启动器需要自己的 JVM,org.gradle.java.home 管不到它——不设 JAVA_HOME 直接报 JAVA_HOME is not set,这是本工程最高频的踩坑点。
1.3 源码依赖度盘点
| 上游模块 | 文件形态 | 依赖 | 处置 |
|---|---|---|---|
test/(共享库) | 26 个 Kotlin 文件,其中 Compose UI 约 100%,expect 仅 3 处(Platform / Resources / Extensions) | Compose 1.2.1 M2 + precompose + ktor + icons | 16 个文件移植进 example/composeApp(M3 化);Extensions.kt(ByteArray.toImageBitmap)未移植——唯一消费者是 Ktor 网络图 |
androidApp/ | Android 壳 | AGP 7.3.1 | 留盘不进 settings |
iosApp/ | iOS 壳 + Pods | Xcode / cocoapods | 留盘不进 settings |
buildSrc/ | 老式版本目录 | 服务旧 Android 构建 | 删除(toml 接管) |
expect 盘点结论:上游 expect 只有 Platform(平台名)、Resources(本地位图)、Extensions(Ktor 字节流转位图)三处平台胶水,其余 100% 是 Compose UI——没有可提取的非 UI 内核,这直接决定路线选择(见第 3.1 节与决策 1)。
1.4 页面边界
顶部栏 M3 Scaffold,TopAppBar + 左上 ☰(抽屉开关,文本字形)
抽屉 M3 ModalNavigationDrawer + NavigationDrawerItem × 10(Demo 列表)
主题 橙色主色 M3 lightColorScheme / darkColorScheme(上游 M2 配色迁移)
内容区 10 个 Demo 页按选中项切换(状态导航)
生命周期 onPageShow / onPageHide / onBackPressed 由 ArkTS 转发 controller
页面启动时 ArkTS 先 nativeApi.MainArkUIViewController() 拿到场景控制器,再交给 Compose 组件渲染——首帧出现前没有任何 ArkTS 业务 UI。
第 2 阶段:目标与依赖打通
2.1 制品线验证(本适配的成败手)
先查社区 Maven(https://maven.eazytec-cloud.com)上有没有 Compose 的 ohos 制品。用 .module 元数据逐个验证:
org.jetbrains.compose:compose (Gradle 插件): 1.9.2-1.1.0-13
runtime / foundation / material3 / ui / components-resources: ohosArm64/ohosX64 klib 齐全
org.jetbrains.compose.export:export: 1.9.2-1.1.0-13
导出 ComposeArkUIViewController 等 ArkUI 桥符号(KN sharedLib 导出用)
skiko-ohos: 0.9.22.2-1.1.0-13
skia 自绘后端(rendererBackend=skia)
五个关键结论:其一,社区 CMP 分支在 1.9.2-1.1.0-13 线上 klib 齐全,"全 Compose UI"可行;其二,Material 3 有 ohos klib、Material 2 没有——Demo 必须从 M2 移植到 M3;其三,moe.tlaster:precompose 无 ohos 变体(中央仓元数据只是镜像),导航要换实现;其四,io.ktor 无 ohos 引擎,网络图降级;其五,material-icons-core 无 ohos klib,两个图标换文本字形。制品线一确认,"不重写 UI"的唯一路线就是社区 CMP 分支。
2.2 工具链钉版(为什么是 2.2.21-1.1.0-05 + 8.14.1)
社区在 Compose-for-OHOS 1.9.2-1.1.0-13 线上配套的 Kotlin 是 2.2.21 定制线;Gradle 用 8.14.1(2.2.21 线经验值,KStore 那条 2.4.20 线才允许 Gradle 9.x,两条线的经验不能互相套用)。settings.gradle.kts 把 eazytec 仓库写在最前:
pluginManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-releases/")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
gradlePluginPortal()
google()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-releases/")
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
google()
mavenCentral()
}
}
rootProject.name = "ohos_multiplatform_compose"
// 上游模块(androidApp、test、iosApp)留盘但不 include:
// 它们钉死在 2022 工具链(Kotlin 1.7.20 / Compose 1.2.1 / AGP 7.3.1),不参与鸿蒙构建。
include(":example:composeApp")
根构建收敛为极简 ohos 构建:
plugins {
alias(libs.plugins.kotlin.multiplatform) apply false
alias(libs.plugins.jetbrains.compose) apply false
alias(libs.plugins.compose.compiler) apply false
}
allprojects {
group = "com.rouge41.kmm.compose"
version = providers.gradleProperty("version").orElse("0.0.1-ohos.1").get()
}
subprojects {
tasks.matching { it.name.contains("Ohos", ignoreCase = true) && it.name.contains("Test") }
.configureEach { enabled = false }
}
gradle.properties 钉住 KN 与渲染后端属性:kotlin.native.ignoreDisabledTargets=true、kotlin.native.cacheKind=none、rendererBackend=skia、ohosSkikoVersion=0.9.22.2-1.1.0-13、version=0.0.1-ohos.1。
2.3 编译 ohos 目标
example/composeApp/build.gradle.kts 是本 fork 的技术主体:
plugins {
alias(libs.plugins.kotlin.multiplatform)
alias(libs.plugins.jetbrains.compose)
alias(libs.plugins.compose.compiler)
}
kotlin {
jvmToolchain(21)
listOf(ohosArm64(), ohosX64()).forEach { ohosTarget ->
ohosTarget.binaries.sharedLib {
baseName = "kn"
export(libs.compose.multiplatform.export) // 导出 ArkUI 桥符号
linkerOpts("-lz")
}
ohosTarget.compilations.getByName("main").apply {
cinterops.create("resource") { // OH_ResourceManager(rawfile)
defFile(file("src/ohosMain/cinterop/resource.def"))
includeDirs(file("src/ohosMain/cinterop/include"))
}
}
}
sourceSets {
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material3)
implementation(compose.ui)
}
val ohosMain by creating {
dependsOn(commonMain.get())
dependencies { api(libs.compose.multiplatform.export) }
}
val ohosArm64Main by getting { dependsOn(ohosMain) }
val ohosX64Main by getting { dependsOn(ohosMain) }
}
}
export(...) 是关键:KN sharedLib 默认不导出依赖里的符号,不 export 的话 libkn.so 里找不到 ComposeArkUIViewController,宿主侧 dlsym 直接失败。实测(本机 4 分 16 秒):
:example:composeApp:linkDebugSharedOhosArm64 BUILD SUCCESSFUL
:example:composeApp:linkDebugSharedOhosX64 BUILD SUCCESSFUL
:example:composeApp:publishDebugBinariesToHarmonyApp BUILD SUCCESSFUL
debugShared 未 strip 时 libkn.so arm64 约 83 MB、x86_64 约 80 MB(含 Compose 运行时与 skiko,HAP 打包时 release strip 后显著缩小)。
2.4 产物落位(Gradle 与 hvigor 的唯一连接点)
// 为 debug/release 两档各注册一个 Copy 任务,把 KN 产物发布进 HAP 工程
arrayOf("debug", "release").forEach { type ->
val capitalized = type.replaceFirstChar { it.uppercase() }
tasks.register<Copy>("publish${capitalized}BinariesToHarmonyApp") {
group = "harmony"
dependsOn("link${capitalized}SharedOhosArm64", "link${capitalized}SharedOhosX64")
duplicatesStrategy = DuplicatesStrategy.INCLUDE
into(rootProject.file("example/harmonyApp"))
from("build/bin/ohosArm64/${type}Shared/libkn_api.h") { into("entry/src/main/cpp/include/arm64-v8a/") }
from("build/bin/ohosArm64/${type}Shared/libkn.so") { into("entry/libs/arm64-v8a/") }
from("build/bin/ohosX64/${type}Shared/libkn_api.h") { into("entry/src/main/cpp/include/x86_64/") }
from("build/bin/ohosX64/${type}Shared/libkn.so") { into("entry/libs/x86_64/") }
}
}
libkn.so 与 libkn_api.h 必须来自同一次 link(ABI 目录一一对应),CMake 按 ABI 选头文件——so 是产物不进 git,每次构建可复现。
第 3 阶段:Demo 源码移植(技术难点)
3.1 路线选择:为什么是"全 Compose UI"
社区已过审的适配有三条主路线:
| 路线 A:内核提取(qrose / Cloudy / Sketchy) | 路线 B:纯 Kotlin 库真编译(KStore) | 路线 C:全 Compose UI(lazytable / Reorderable / Haze,本 fork) | |
|---|---|---|---|
| 适用对象 | 库里有纯 Kotlin 内核(编码器、求解器) | 库本身零 UI、依赖 kotlinx 制品线 | 库 / 应用的价值就是 Compose UI 本身 |
| 做法 | Compose 源码停放不编译,内核编成 so,ArkTS / Canvas 重绘 | commonMain 原样编译成 ohos KLIB,ArkTS 只做验收宿主 | Compose 源码直接为 ohos 目标编译,社区 CMP 分支 + skia 在设备上渲染真实场景 |
| UI 保真 | 视觉近似重实现 | 无 UI 面 | 同一份代码同一份渲染 |
本项目 26 个 Kotlin 文件里只有 3 个 expect/actual 算平台胶水,其余 100% 是 Compose UI——走路线 A 等于把整个 App 用 ArkTS 重写,既丢掉"一份 Compose UI 跑多端"的演示语义,也放大改动面;制品盘点(第 2.1 节)确认 CMP 1.9.2-1.1.0-13 ohos 线齐全后,路线 C 是唯一不需要重写 UI 的做法。
3.2 Material 2 → Material 3 逐文件 diff
上游 Demo 写在 Compose 1.2 / M2 上,M2 无 ohos klib、M3 有,按 M3 API 移植(语义不变):
| 文件 | 改动 |
|---|---|
App.kt | PreCompose NavHost → 状态导航;M2 Scaffold / ModalDrawer → M3 ModalNavigationDrawer / Scaffold;Icons.Default.Menu → 文本字形 ☰ |
Theme.kt | M2 lightColors / darkColors → M3 lightColorScheme / darkColorScheme(橙色主色保留) |
Content.kt | 去掉 Ktor import(本来没用到);暗色 / 安全区 CompositionLocal 原样保留 |
Buttons.kt | M2 → M3 API(ButtonDefaults.buttonElevation 等),注释保留 |
BottomNavigationDemo.kt | M2 BottomNavigation / Item → M3 NavigationBar / Item;NavHost → 状态切换;Icons.Default.Home → ⌂ |
LazyColumnDemo.kt | Divider → HorizontalDivider |
TextFields.kt | import 换 M3,API 兼容 |
Images.kt | 去掉 Ktor 下载;AsyncImage 变占位说明;本地图走 Canvas 位图 |
HelloPlatform / Layout / Lorem / TextStyles / Counter.kt | 仅 import 从 M2 换 M3,逻辑零改动 |
Platform.kt(expect) | 原样;ohos actual 返回 OpenHarmony (Kotlin/Native) |
Resources.kt(expect) | 原样;ohos actual 用 Canvas 画 200×300 位图 |
Extensions.kt(ByteArray.toImageBitmap) | 未移植(唯一消费者是 Ktor 网络图,ohos 无引擎) |
3.3 无制品依赖的降级策略
// 上游:PreCompose NavHost + rememberNavigator() 跨页导航
// 本 fork:mutableStateOf<Demo> 状态切换,抽屉交互语义保留
var currentDemo by remember { mutableStateOf<Demo>(Demo.HelloPlatform) }
ModalNavigationDrawer(drawerContent = { /* 10 个 NavigationDrawerItem */ }) {
Scaffold(topBar = { TopAppBar(title = { Text(currentDemo.name) },
navigationIcon = { TextButton(onClick = { drawerOpen = true }) { Text("☰") } }) }) {
when (currentDemo) { Demo.HelloPlatform -> HelloPlatform() /* ... */ }
}
}
降级三原则:导航语义保留(抽屉开合、页面切换一个不少)、不引入伪造依赖(网络图显示占位说明而不是假图)、不为一两个图标 vendor 整库(icons 换字形)。Images 页本地位图用 Canvas 现画(ImageBitmap 语义保留),资源管线就绪后可换 rawfile 真图。
3.4 ohos actual 与场景入口
// example/composeApp/src/ohosMain/.../Platform.kt
actual val platform: Platform = object : Platform {
override val name: String get() = "OpenHarmony (Kotlin/Native)"
}
// example/composeApp/src/ohosMain/.../test/MainArkUIViewController.kt
@CName("MainArkUIViewController")
fun MainArkUIViewController(env: CPointer<OpaquePointer>?): CPointer<OpaquePointer>? {
initMainHandler(env) // KN 协程主线程挂到 ArkTS 线程
return ComposeArkUIViewController(env) { Content() } // compose-export 提供
}
Content() 是上游共享库的入口组合函数(Theme → App),鸿蒙侧原样复用——这就是"同一份 Compose UI"的直接证据。resource cinterop 提供 OH_ResourceManager 绑定(raw_file.h / raw_file_manager.h),为 compose 资源管线预留 rawfile 入口。
第 4 阶段:原生桥接(技术难点)
4.1 渲染链路(一行调用栈)
ArkTS Index.ets
└─ nativeApi.MainArkUIViewController() // libentry.so (CMake NAPI 胶水)
└─ @CName("MainArkUIViewController") // libkn.so (Kotlin/Native 导出)
├─ initMainHandler(env) // KN 协程主线程挂到 ArkTS 线程
└─ ComposeArkUIViewController(env) { Content() } // compose-export 提供
└─ Compose 场景树(App → Theme → 10 个 Demo)
└─ Compose({ controller, libraryName: 'entry' }) // @cpf-kmp-cmp/compose 的 ArkUI 组件
└─ skia 自绘(skiko-ohos 0.9.22.2-1.1.0-13)→ 屏幕
ArkTS 永远不直接 import libkn.so,只 import libentry.so;libkn.so 由 CMake 以 IMPORTED 目标链进 libentry.so,dlopen 时互相可见。controller 返回后同时接管生命周期:onPageShow / onPageHide / onBackPressed 都转发给它。
4.2 C++ N-API 注册
EXTERN_C_START
static napi_value Init(napi_env env, napi_value exports) {
androidx_compose_ui_arkui_init(env, exports); // compose.har 的 ArkUI 初始化入口
napi_property_descriptor desc[] = {
{"MainArkUIViewController", nullptr, MainArkUIViewController,
nullptr, nullptr, nullptr, napi_default, nullptr},
/* ...渲染节点回调注册(nativeCreateRootNode / nativeSetPixelRatio 等)... */
};
napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);
return exports;
}
EXTERN_C_END
static napi_module demoModule = {
.nm_version = 1, .nm_flags = 0, .nm_filename = nullptr,
.nm_register_func = Init, .nm_modname = "entry", // 必须与 libraryName:'entry' 配对
.nm_priv = ((void*)0), .reserved = {0},
};
MainArkUIViewController 回调只做一件事:把 N-API env 透传给 KN 导出函数并 reinterpret 返回的 controller——所有场景构建都在 Kotlin 侧。
4.3 CMake:IMPORTED + skikobridge
if(CMAKE_OHOS_ARCH_ABI STREQUAL "x86_64" OR OHOS_ARCH STREQUAL "x86_64")
set(LIBKN_ABI_DIR "x86_64")
else()
set(LIBKN_ABI_DIR "arm64-v8a")
endif()
find_package(compose) # compose.har 提供的桥包
add_library(entry SHARED napi_init.cpp)
add_library(ComposeApp SHARED IMPORTED)
set_target_properties(ComposeApp PROPERTIES
IMPORTED_LOCATION "${CMAKE_CURRENT_SOURCE_DIR}/../../../libs/${LIBKN_ABI_DIR}/libkn.so")
target_link_libraries(entry PUBLIC ComposeApp)
target_link_libraries(entry PUBLIC compose::skikobridge) # 先链 libkn.so,再链 skikobridge
target_link_libraries(entry PUBLIC libace_napi.z.so libhilog_ndk.z.so
libdeviceinfo_ndk.z.so librawfile.z.so
${EGL-lib} ${GLES-lib} ${libace-lib} ${libnapi-lib} ${libuv-lib} libc++_shared.so)
链接顺序有讲究:libkn.so(Compose 场景)在前、compose::skikobridge(skia → ArkUI 的桥)在后,反了会出未解析符号。abiFilters: [arm64-v8a, x86_64] 双 ABI,每个 ABI 三件套(libentry.so / libkn.so / libc++_shared.so)缺一不可。
第 5 阶段:ArkTS 宿主
5.1 Index.ets(全部 ArkTS 业务代码)
import { ArkUIViewController, Compose } from '@cpf-kmp-cmp/compose';
import nativeApi from 'libentry.so';
@Entry
@Component
struct Index {
private controller: ArkUIViewController | undefined = undefined;
@State errorMessage: string = 'Native module not ready';
aboutToAppear() {
// 带防御:libentry.so 未编译 / module 名不配对时给出可读错误而不是白屏
try {
this.controller = nativeApi.MainArkUIViewController();
if (!this.controller) this.errorMessage = 'Controller creation failed (returned null)';
} catch (e) {
this.errorMessage = 'Exception creating controller: ' + JSON.stringify(e);
}
}
build() {
Column() {
if (this.controller) {
Compose({
controller: this.controller,
libraryName: 'entry', // 与 CMake nm_modname 配对
onBackPressed: () => this.controller!.onBackPress()
})
} else {
Text(this.errorMessage)
}
}.width('100%').height('100%')
}
onPageShow() { this.controller?.onPageShow(); }
onPageHide() { this.controller?.onPageHide(); }
onBackPressed(): boolean { return this.controller ? this.controller.onBackPress() : false; }
}
5.2 compose.har 的 vendor 纪律
@cpf-kmp-cmp/compose(1.9.2-0.3.0,约 1.1 MB)以 compose.har 形式随仓 vendor:桥包是源,进 git;KN so 是产物,不进 git(社区模板惯例,cmp_qrose / lazytable 同款)。.gitignore 里 *.har 全局忽略、仅对 entry/libs/compose.har 反转例外;entry/libs/arm64-v8a| 与 x86_64 下的 libkn.so 由 publishDebugBinariesToHarmonyApp 每次构建回填。签名材料(signingConfigs 带 material 的完整配置)绝不进仓,仓库内保持空数组,勾选 DevEco 自动签名后由 IDE 回填。
第 6 阶段:示例与验证
6.1 工程结构
example/
├── composeApp/
│ ├── build.gradle.kts 双 ABI sharedLib(baseName=kn)+ export + resource cinterop + publish 任务
│ └── src/
│ ├── commonMain/.../test/ 16 个移植源文件(App/Content/Theme + 10 Demo)
│ └── ohosMain/
│ ├── kotlin/.../MainArkUIViewController.kt @CName 场景入口
│ └── cinterop/ resource.def + raw_file*.h(OH_ResourceManager)
└── harmonyApp/
├── build-profile.json5 signingConfigs 留空(DevEco 自动签名回填)
├── AppScope/app.json5 bundleName org.terminator.ohos.multiplatformcompose、0.0.1-ohos.1
└── entry/
├── build-profile.json5 externalNativeOptions + abiFilters [arm64-v8a, x86_64]
├── libs/compose.har @cpf-kmp-cmp/compose(进 git)
├── libs/{arm64-v8a,x86_64}/ publish 任务回填的 libkn.so(gitignore)
├── src/main/cpp/CMakeLists.txt IMPORTED 引入 KN so + skikobridge
├── src/main/cpp/napi_init.cpp N-API 导出 + androidx_compose_ui_arkui_init
├── src/main/cpp/include/<abi>/ publish 任务回填的 libkn_api.h(gitignore)
├── src/main/cpp/types/libentry/ Index.d.ts + oh-package.json5
└── src/main/ets/pages/Index.ets Compose({controller, libraryName:'entry'})
6.2 构建、签名和安装
$env:JAVA_HOME = "C:\Program Files\Eclipse Adoptium\jdk-21.0.12.101-hotspot"
.\gradlew.bat :example:composeApp:publishDebugBinariesToHarmonyApp # 双 ABI link + so 落位
然后 DevEco Studio 打开 example/harmonyApp(注意是 harmonyApp 目录):sync(首次自动 ohpm install)→ File → Project Structure → Signing Configs 勾选 Automatically generate signature(华为账号登录,证书写本机 ~/.ohos/config,不进仓库)→ Build → Build Hap(s)。
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.multiplatformcompose -m entry
真机 = arm64;DevEco 模拟器 = x86_64,双 ABI 已保证不报安装错误 9568347。
四、完整代码对照
4.1 整体架构
com.rouge41.kmm.compose.test/**(上游 Demo,移植进 example/composeApp)
│ App(抽屉 + 状态导航)/ Content / Theme / 10 个 Demo
MainArkUIViewController.kt
│ @CName("MainArkUIViewController") + initMainHandler
libkn.so(KN sharedLib,export compose-export)
│ ComposeArkUserController(env) { Content() }
libentry.so
│ androidx_compose_ui_arkui_init + MainArkUIViewController 注册
@cpf-kmp-cmp/compose(compose.har)
│ Compose({ controller, libraryName: 'entry' })
Index.ets
│ 生命周期转发(onPageShow / onPageHide / onBackPressed)
▼
skia 自绘(skiko-ohos 0.9.22.2-1.1.0-13)→ ArkUI 屏幕
4.2 文件清单
| 文件 | 职责 |
|---|---|
settings.gradle.kts | 极简 ohos 构建、eazytec Maven 置前、ohos_multiplatform_compose、上游模块移出 |
gradle/libs.versions.toml | Kotlin 2.2.21-1.1.0-05 + CMP 1.9.2-1.1.0-13 + 三插件别名 |
build.gradle.kts(根) | 版本 0.0.1-ohos.1、全局禁用 Ohos*Test* 任务 |
gradle.properties | JDK21 / KN 属性 / rendererBackend=skia / ohosSkikoVersion |
example/composeApp/build.gradle.kts | 双 ABI sharedLib、export 导出、resource cinterop、publish Copy 任务 |
example/composeApp/src/commonMain/** | 16 个移植源文件(M3 化,逻辑零改动) |
example/composeApp/src/ohosMain/**/MainArkUIViewController.kt | @CName 场景入口 + initMainHandler |
example/composeApp/src/ohosMain/cinterop/** | OH_ResourceManager 绑定(rawfile 预留) |
example/harmonyApp/entry/src/main/cpp/CMakeLists.txt | IMPORTED 引入 KN so、compose::skikobridge 链接 |
example/harmonyApp/entry/src/main/cpp/napi_init.cpp | N-API 导出、androidx_compose_ui_arkui_init |
example/harmonyApp/entry/src/main/ets/pages/Index.ets | ArkTS 宿主(装载 + 生命周期转发) |
README.OpenHarmony_CN.md | 鸿蒙适配主文档(19 节) |
4.3 关键 API 对照
| 层次 | API | 作用 |
|---|---|---|
| Compose | Content() | 上游入口组合函数(Theme → App),ohos 原样复用 |
| Compose | ModalNavigationDrawer / NavigationBar | M3 抽屉与底部导航(替代 M2 ModalDrawer / BottomNavigation) |
| Compose | lightColorScheme / darkColorScheme | 橙色 M3 主题(替代 M2 lightColors / darkColors) |
| KN 导出 | @CName("MainArkUIViewController") | C ABI 场景入口(env 透传,controller 返回) |
| KN 导出 | initMainHandler(env) | KN 协程主线程挂到 ArkTS 线程 |
| compose-export | ComposeArkUIViewController(env) { Content() } | 构建 Compose 场景控制器 |
| N-API | androidx_compose_ui_arkui_init | compose.har 的 ArkUI 桥初始化 |
| ArkTS | Compose({ controller, libraryName }) | 把 controller 交给 ArkUI 组件渲染 |
| ArkTS | onPageShow / onBackPressed | 生命周期转发给 controller |
4.4 ArkTS 与 Kotlin 的边界
// ArkTS 侧:只拿 controller、只转发生命周期,一行业务 UI 都没有
this.controller = nativeApi.MainArkUserController();
Compose({ controller: this.controller, libraryName: 'entry',
onBackPressed: () => this.controller!.onBackPress() })
// Kotlin 侧:上游场景原样进 ohos,@CName 入口只有 3 行
@CName("MainArkUIViewController")
fun MainArkUIViewController(env: CPointer<OpaquePointer>?): CPointer<OpaquePointer>? {
initMainHandler(env)
return ComposeArkUIViewController(env) { Content() }
}
两者之间只传 env 与 controller 指针,不传 JSON、不传 Kotlin 对象——这是路线 C 与 KStore 那种 JSON 桥的根本区别:UI 场景整体活在 Kotlin 侧,ArkTS 只是宿主。
五、关键决策说明
决策 1:走路线 C(全 Compose UI)而不是内核提取
本项目 26 个文件 100% 是 Compose UI,没有 qrose 的编码器、KStore 的存储引擎那样的"纯 Kotlin 内核"可提取;走路线 A 等于全部 ArkTS 重写,丢掉演示语义还放大改动面。制品盘点确认 CMP 1.9.2-1.1.0-13 ohos 线齐全后,路线 C 是唯一"不重写 UI"的做法——16 个小文件升 API 的移植成本远小于重写。
决策 2:移植进 example/composeApp,不原地升级 :test
上游模块留盘不进 settings 是社区目录规则(cmp_qrose 确立);原地升级会把 2022 老工具链(AGP 7.3.1 / jcenter)强拖进鸿蒙构建,两个工具链互斥。buildSrc/ 直接删除——Gradle 会无条件自动构建它,版本管理由 gradle/libs.versions.toml 接管。
决策 3:Material 2 → Material 3
M2 无 ohos klib、M3 有(lazytable 已验证);M3 是社区线的事实标准。改动集中在 import 与废弃 API(NavigationDrawerItem / NavigationBar / HorizontalDivider / buttonElevation),10 个 Demo 的逻辑零改动,橙色主色保留。
决策 4:PreCompose NavHost → 状态导航 + M3 ModalNavigationDrawer
precompose 无 ohos 制品。抽屉交互语义完整保留(开合、选中态、页面切换),只是路由实现从 NavHost 换成 mutableStateOf<Demo>——对 Demo 而言两者可观察行为一致;precompose 出制品后可换回(改动集中在 App.kt / BottomNavigationDemo.kt)。
决策 5:compose.har 进 git,libkn.so 不进
社区模板惯例:桥包是源(@cpf-kmp-cmp/compose 1.9.2-0.3.0,约 1.1 MB,ohpm 解析依赖),KN so 是产物(debugShared 单 ABI 约 80 MB,每次构建可复现)。.gitignore 全局忽略 *.so / *.har,仅对 entry/libs/compose.har 反转例外;签名材料绝不进仓。
六、测试与验证
6.1 测试环境
本次验证使用 Windows 11(x64)、JDK 21(Adoptium jdk-21.0.12.101-hotspot)、DevEco Studio 6.0.0 Release(HarmonyOS SDK 兼容 5.0.5(17),模拟器系统镜像 HarmonyOS 7.0.0 / API 26)、OpenHarmony 命令行工具(hvigor / ohpm / hdc)与鸿蒙真机(arm64)。
6.2 编译与链接验证
:example:composeApp:linkDebugSharedOhosArm64 BUILD SUCCESSFUL
:example:composeApp:linkDebugSharedOhosX64 BUILD SUCCESSFUL
:example:composeApp:publishDebugBinariesToHarmonyApp BUILD SUCCESSFUL(4m16s)
→ entry/libs/arm64-v8a/libkn.so(约 83 MB,debugShared 未 strip)
→ entry/libs/x86_64/libkn.so(约 80 MB,debugShared 未 strip)
→ entry/src/main/cpp/include/<abi>/libkn_api.h
链接日志里 i: 级的 unlinked class symbol 'org.jetbrains.skiko/ExternalRenderNode' 提示是社区 CMP 分支 fusion renderer 的信息级输出(对应融合渲染特性,本 Demo 用 skia 自绘后端不走该路径),不影响产物与运行。
6.3 HAP 验证
BuildNativeWithCmake: finished(双 ABI 编出 libentry.so,链 libkn.so + skikobridge)
CompileArkTS: finished
PackageHap: finished(双 ABI 三件套 so 进包)
BUILD SUCCESSFUL
install bundle successfully
start ability successfully
DevEco 打开 example/harmonyApp 后 sync → 自动签名 → Build Hap 全链路通过。
6.4 功能用例(真机逐项验收)
- 启动进入 Compose 界面,顶部栏显示
☰+ 当前 Demo 名,首帧无白屏; - 抽屉开合正常,10 个 Demo 逐项可切换;
- HelloPlatform 居中显示
Hello, OpenHarmony (Kotlin/Native)!(Platform actual 生效); - Layout 页彩色 Box / Column / Row / Spacer 按上游配色排布(绿 / 品红 / 黄 / 红 / 黑 / 蓝);
- LazyColumn 25 行懒加载 + 分隔线,快速滑动不丢帧;
- Counter 的
+/-改数字,状态保留; - Buttons 6 种按钮全部可点;
- TextFields 弹软键盘、实时回显;
- BottomNavigation 三 Tab 切换,选中态高亮;
- 返回键经
onBackPressed转发 controller,默认行为退出应用。
6.5 验证结论
Compose 编译器、场景树、社区 CMP 分支 klib、KN 动态库导出、CMake / N-API 桥、ArkTS 宿主和 HAP 已完成链路验证。应用在鸿蒙真机上成功安装并进入前台,抽屉导航与 10 个 Material 演示页全部由同一份 Compose 代码渲染(见下节效果图)——"一份 Compose UI 跑 Android + iOS + OpenHarmony"的三端证明成立。
七、运行效果
7.1 真机截图

7.2 命令速查
git clone https://atomgit.com/oh-tpc/multiplatform-compose.git
cd multiplatform-compose
export JAVA_HOME="/path/to/jdk-21" # Windows: $env:JAVA_HOME="...jdk-21..."
./gradlew :example:composeApp:publishDebugBinariesToHarmonyApp # 双 ABI link + so 落位
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.multiplatformcompose -m entry
hdc shell hilog | grep Compose
八、遗留问题与改进方向
8.1 踩坑复盘
gradlew.bat报JAVA_HOME is not set:启动器需要自己的 JVM,org.gradle.java.home管不到它——先set JAVA_HOME=...JDK21再跑 gradlew;- 配置期
Unresolved reference: ohosArm64:eazytec 双仓没放在pluginManagement/dependencyResolutionManagement最前,置前即解; - M2 API 编译不过(
ListItem(text=)、rememberScaffoldState、BottomNavigation…):compose M2 无 ohos 变体,只能依赖 M3,按 M3 API 移植; - 图标 import 失败:material-icons 无 ohos klib,文本字形(
☰/⌂)替代,不为一两个图标 vendor 整库; - KN sharedLib 找不到
ComposeArkUIViewController:默认不导出依赖符号,必须export(libs.compose.multiplatform.export); - CMake 链接顺序:
libkn.so(IMPORTED)在前、compose::skikobridge在后,反了出未解析符号; - ArkTS 拿不到 controller(
nativeApi is undefined):CMakenm_modname必须是entry,与Compose({libraryName:'entry'})配对; - 启动白屏 / 黑屏:多为
libkn.so与libentry.so不来自同一次构建——重跑 publish 后再 assembleHap,hdc shell hilog | grep Compose定位; - x86_64 模拟器装 HAP 报
9568347:HAP 缺当前 ABI 的 so,abiFilters双 ABI 且每个 ABI 三件套缺一不可; buildSrc/必须删除而不是留盘:Gradle 无条件自动构建它,老版本目录会把 2022 工具链拖进鸿蒙构建;- 上游模块被误拖进构建:settings 显式 include 了它们——本 fork 只 include
:example:composeApp; - 根构建出现 ohos Test 任务失败:鸿蒙 Kotlin 跑不了这些测试,根 build 全局禁用
*Ohos*Test*任务(沿用社区模板); - DevEco 打开错目录无法运行:必须打开
example/harmonyApp(HAP 工程),打开example/composeApp(Gradle 模块)或仓库根目录都没有运行配置; - 签名材料由 DevEco 自动写入
build-profile.json5:仓库内保持空数组,证书绝不进 AtomGit。
8.2 已知问题
- DarkMode / SafeArea 未接系统真值:上游在 iOS 用 Swift 回调注入安全区和暗色模式;ohos 侧
safeAreaState恒为零 Padding、darkmodeState取isSystemInDarkTheme()运行时返回,视觉上不影响 Demo; - 网络图片不可用:Images 页 AsyncImage 显示占位说明(Ktor 无 ohos 引擎制品),本地图由 Canvas 现画承担;
- 无返回键拦截:上游靠抽屉导航未注册返回栈,ohos 返回键由 ArkTS
onBackPressed转发(默认行为退出应用); - M3 视觉代际差异:按钮 / Scaffold / 抽屉按 Material 3 规范渲染,与上游 2021 年 M2 截图存在代际差异,橙色主色保留;
- 上游 Android / iOS 构建不在本仓 settings 中:想构建上游原始双端 Demo,
git checkout上游提交或临时把:androidApp/:test加回 settings(需自带旧工具链,与鸿蒙构建互斥); - HAP 安装必须使用与 bundle name
org.terminator.ohos.multiplatformcompose匹配的签名。
8.3 未来优化方向
- precompose 出 ohos 制品后,把状态导航换回 NavHost(改动集中在 App.kt / BottomNavigationDemo.kt);
- ktor 或其他网络栈出 ohos 引擎后,恢复 AsyncImage 真下载;
- 把 DarkMode / SafeArea 接上 ArkTS 回调(EntryAbility 监听
onConfigurationUpdate→ native 通知 KN 更新 CompositionLocal); - compose resources 管线就绪后,用 rawfile 真·资源图替换 Canvas 位图(cinterop 已预留);
- 视社区节奏评估升级到
1.12.0-0.1.0-07线(Kotlin 2.4.20),届时 M2 / material-icons 制品或已补齐,可回迁更接近上游的写法; - 在 CI 中加入 ohos 编译、ELF 符号检查和 HAP 构建;补充模拟器(x86_64)验收矩阵和自动化截图脚本。
九、总结
9.1 核心难点回顾
上游 :test 共享库(Compose 1.2 / M2 / PreCompose / Ktor)
→ 查重定位:无可提取内核 → 路线 C 全 Compose UI
→ 制品盘点:CMP 1.9.2-1.1.0-13 ohos 线齐全 / M2、precompose、ktor、icons 无制品
→ 工具链钉版:Kotlin 2.2.21-1.1.0-05 + Gradle 8.14.1 + JDK 21(eazytec 双仓置前)
→ 根构建替换(cmp_qrose 模式)+ buildSrc 删除
→ 16 个源文件移植:M2→M3、导航状态化、Ktor 占位、图标字形化
→ ohosArm64 / ohosX64 sharedLib + export 导出 + resource cinterop
→ CMake IMPORTED + skikobridge + N-API(androidx_compose_ui_arkui_init)
→ ArkTS 宿主(Compose({controller, libraryName}) + 生命周期转发)
→ DevEco HAP → 自动签名 → 真机验收(10 个 Demo 逐项过)
9.2 封装层次
- Compose 层:上游原版的 App / Content / Theme 与 10 个 Demo(M3 化,逻辑零改动);
- 导出层:
@CName("MainArkUIViewController")+initMainHandler+ compose-export 的ComposeArkUserController; - Native 层:双 ABI
libkn.so(export 桥符号)+libkn_api.h; - N-API 层:
libentry.so注册导出函数、初始化 ArkUI 桥; - ArkTS 层:装载 controller、转发生命周期、错误兜底显示;
- DevEco 层:完成 CMake、HAP、签名、安装和真机运行。
9.3 三条经验
- 先验证生态制品线再定适配路线:CMP ohos 线在
1.9.2-1.1.0-13齐全是路线 C 的前提,M2 / precompose / ktor / icons 的缺位决定了移植面——路线选择必须基于当天的制品可达性,而不是历史结论; - 工具链版本跟着依赖走、按线区分经验:Compose
1.9.2-1.1.0-13线配 Kotlin2.2.21-1.1.0-05+ Gradle 8.14.1;KStore 那条 2.4.20 线才允许 Gradle 9.x——两条线的经验不能互相套用; - 验收以"真跑起来的 Demo"为准:UI 库 / Demo 型项目的交付物是可安装的 HAP 与真机上逐项可点的演示页,"编译通过"只占验收链路的三分之一——链接、落位、签名、安装、运行、十个页面逐项过才算闭环。
9.4 适配成果
当前适配已完成:
- 上游 Demo 的双 ABI Kotlin/Native 编译(16 个源文件 M3 化移植,逻辑零改动;
publishDebugBinariesToHarmonyApp实测 BUILD SUCCESSFUL,双 ABIlibkn.so+libkn_api.h自动落位); - 社区 Compose-for-OHOS 分支
1.9.2-1.1.0-13(skia 自绘)在演示 App 上的完整落地; @CName场景入口 +export导出 + CMake / N-API 桥(androidx_compose_ui_arkui_init+ skikobridge 链接);- DevEco hvigor HAP 工程(vendored
compose.har、双 ABI 三件套 so、ArkTS 宿主 + 生命周期转发); - 真机(arm64)安装、运行与逐项验收:抽屉导航 + 10 个 Material 演示页全部由同一份 Compose 代码渲染(见第七节效果图);
- AtomGit 项目文档、OpenHarmony README(中英双份,中文主文档 19 节)和本地效果图。
适配过程 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)