【KMP for OpenHarmony】-- Apollo-Kotlin
01 · Apollo-Kotlin 的鸿蒙适配
本文是「Apollo-Kotlin 鸿蒙适配」系列第一篇,讲怎么把上游库改造成能在 HarmonyOS 上编译。
阅读前提:了解 Kotlin Multiplatform 的expect/actual与 target 概念。
〇、环境与上游
0.1 环境搭建指引
本文不重复官方环境搭建步骤,按 CPF(CPF-KMP-CMP 社区)官方文档准备即可:
| 内容 | 链接 |
|---|---|
| 快速开始(15 分钟:装工具 → 配签名 → 跑第一个鸿蒙应用) | CPF 文档 · 快速开始 |
| 工具链插件安装、工程模板、运行调试 | CPF 文档 · 工具链介绍 |
工具链插件离线安装包(KMP OHOS Support-1.0.0.zip) | https://maven.eazytec-cloud.com/nexus/repository/file-storage/ohos-kmp-toolchain/1.0.0/KMP OHOS Support-1.0.0.zip |
| 工具链插件源码 / Issue | https://gitcode.com/CPF-KMP-CMP/ohos-kmp-toolchain |
| 文档总目录 | CPF 文档 · 目录 |
文档仓库的分支名带版本号(当前
main-2.2,对应 KMP2.2.21/ CMP1.9.2)。
工具链换代后分支与链接都会变,上面的链接若 404,从 CPF 组织首页 进docs仓库,切到与工具链版本匹配的分支。
0.2 环境版本(本文验证所用)
| 项 | 版本 / 路径 | 说明 |
|---|---|---|
| JDK | 17 | Android Studio 的 Gradle JDK 必须切到 17 |
| Android Studio | 2024.2.1+ | KMP 工程侧开发、Gradle Sync |
| DevEco Studio | 6.0.0+,本文验证 6.1.0.850 | 鸿蒙壳工程 harmonyApp/、签名、hvigor 打包 |
| HarmonyOS SDK | API 23(工具链要求 API 17+) | 在 DevEco 的 SDK Manager 中下载 |
| OHOS Kotlin 工具链 | 2.2.21-1.1.0-07 | 当前上限,见第二节 |
| KMP OHOS Support 插件 | 1.0.0 | 读取 local.properties 里的 local.deveco.path |
| 目标设备 | HarmonyOS API 17+ 真机或模拟器 | 必须用华为开发者账号完成自动签名才能安装 |
DevEco 装在非默认路径时,必须在工程根目录的 local.properties 里显式声明(该文件不入库):
local.deveco.path=D\:\\Dev\\Huawei\\DevEco Studio
插件只认这一个键;环境变量
TOOL_HOME/OHOS_SDK_HOME不会被读取,漏配会报
Sync failed: DevEco Studio path not found(详见适配问题与解决方案.mdF2)。
0.3 上游地址
| 项 | 地址 |
|---|---|
| 上游仓库 | https://github.com/apollographql/apollo-kotlin |
| 本文所基于的 tag | v4.3.0 |
适配后源码(含 ohos 改动,单 main 分支) | https://gitcode.com/Goway_Hui/Apollo-Kotlin |
适配 diff(相对 v4.3.0) | adapt/patches/apollo-kotlin-4.3.0-ohos.patch |
| 可运行示例 | KMPCMPOHOS @ feat/01-apollo-kotlin |
上游只经
codeload.github.com取源(github.com:443的 git 协议在本机不可达):
curl -sL -o apollo.tar.gz https://codeload.github.com/apollographql/apollo-kotlin/tar.gz/refs/tags/v4.3.0
0.4 CPF 仓库与依赖来源
“CPF”即 CPF-KMP-CMP 社区,本适配从它这里取两样东西:鸿蒙定制 Kotlin 工具链,以及已适配 ohosArm64 变体的传递依赖。
| 用途 | 地址 |
|---|---|
| 组织首页(文档 / 插件 / 已适配库源码) | https://gitcode.com/CPF-KMP-CMP |
| Maven 仓库(Gradle 里要配的那个) | https://maven.eazytec-cloud.com/nexus/repository/maven-public/ |
Maven 浏览页(查某个库有哪些 -1.0.0 定制版本) | https://maven.eazytec-cloud.com/nexus/#browse/browse:maven-releases |
| 三方库总览(上游版本 ↔ 鸿蒙化版本对照表) | CPF 文档 · 三方库 |
| 三方库示例工程 | kmp-thirdlibdemo · kmp-cmp-example |
Gradle 侧只需加一条仓库,后文 3.1 会改到:
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
一、为什么要"适配"
Apollo-Kotlin 是一个标准的多平台库,它在 build-logic 里统一注册编译目标:
// 上游 build-logic/src/main/kotlin/Mpp.kt(改造前)
if (withJvm) { jvm() }
if (enableJs && withJs) { js(IR) { ... } }
if (withWasm) { wasmJs { nodejs() } }
if (enableLinux && withLinux) { linuxX64("linux") }
if (withAndroid) { androidTarget { ... } }
if (enableApple) { appleTargets.forEach { ... } }
没有 ohosArm64。HarmonyOS 上跑的是 Kotlin/Native 产物(.so / klib),
官方 Kotlin 编译器不产出这个目标,必须用定制工具链;库也必须显式注册该目标,
并为每个 expect 声明补上 actual 实现。
所以"适配"就三件事:
二、第一道门槛:版本选型
这是最容易忽略、也最容易卡死的一步。鸿蒙定制 Kotlin 工具链的版本上限决定了能适配哪个上游版本。
查询当前可用版本:
curl -s "https://maven.eazytec-cloud.com/nexus/repository/maven-public/org/jetbrains/kotlin/kotlin-gradle-plugin/maven-metadata.xml" \
| grep -o "<version>[^<]*</version>" | sed 's/<[^>]*>//g' \
| grep -E "^2\.[0-9]+\." | sort -uV | tail
结果(2026-10):
2.2.21-1.1.0-01 ... 2.2.21-1.1.0-07
2.2.21-KBAT-004 ... 2.2.21-KBAT-009
2.2.21-OH.0.1.0-01 ... 2.2.21-OH.0.1.0-07
上限是 2.2.21。对照 Apollo 各版本的 Kotlin 依赖:
| Apollo 版本 | 上游 Kotlin | 结论 |
|---|---|---|
| 5.2.0 / 5.1.0 / 5.0.0 | 2.3.10 | ✗ 超出上限 |
| 4.3.0 | 2.1.20 | ✓ 选用,升级到 2.2.21 |
原则:优先"升上游的 Kotlin 版本",而不是"降上游库版本"。
升级 Kotlin 一般是兼容的;降库版本可能丢掉 API、破坏依赖约束。
最终把 Apollo 4.3.0 的 Kotlin 从 2.1.20 升到 2.2.21-1.1.0-07。
三、改动清单
3.1 构建脚本(4 个文件)
gradle/libraries.toml —— 版本升级
kotlin-plugin = "2.2.21-1.1.0-07" # was 2.1.20
kotlin-plugin-max = "2.2.21" # was 2.1.20
gradle/repositories.gradle.kts —— 加鸿蒙仓库
listOf(pluginManagement.repositories, dependencyResolutionManagement.repositories).forEach {
it.apply {
// OHOS (OpenHarmony) KMP 定制产物仓库
maven("https://maven.eazytec-cloud.com/nexus/repository/maven-public/")
mavenCentral()
...
}
}
build.gradle.kts(根) —— 移除 BCV
binary-compatibility-validator 0.16.3 不认识自定义目标 ohosArm64,
会在配置阶段抛 NPE,必须移除:
// apply(plugin = "org.jetbrains.kotlinx.binary-compatibility-validator")
// configure<kotlinx.validation.ApiValidationExtension> { ... } ← 整块删掉
build-logic/src/main/kotlin/Mpp.kt —— 注册目标
kotlinExtension.apply {
// OHOS 适配:ohosOnly 模式下只保留 ohosArm64 目标,
// 便于在 Windows 上编译/发布(apple/js/wasm 目标在 Windows 上不可构建)
val ohosOnly = providers.gradleProperty("apollo.ohosOnly").orNull == "true"
if (withJvm && !ohosOnly) { jvm() }
if (enableJs && withJs && !ohosOnly) { js(IR) { ... } }
if (withWasm && !ohosOnly) { wasmJs { nodejs() } }
if (enableLinux && withLinux && !ohosOnly) { linuxX64("linux") }
// ---- 新增:鸿蒙目标 ----
if (withOhos) {
ohosArm64 {
binaries {
all {
// 工具链自带 sysroot,含 libhilog_ndk.z.so,只需写库名
linkerOpts("-lhilog_ndk.z")
}
}
}
}
if (withAndroid && !ohosOnly) { androidTarget { ... } }
if (enableApple && !ohosOnly) { appleTargets.forEach { ... } }
configureSourceSetGraph()
configureOhosHierarchy()
}
关键点:linkerOpts 只写 -lhilog_ndk.z。
CPF 官方示例里写的是 ${System.getenv("OHOS_NDK_HOME")}/native/llvm/.../libunwind.a,
但本机 OHOS_NDK_HOME 指向 .../hms/native/BiSheng,拼出来是死路径。
定制工具链自带完整 sysroot(145 个系统库),不需要手动指 NDK 路径。

3.2 源码集层级(最容易踩的坑)
第一版适配时我删掉了手写的 ohosArm64Main.dependsOn(...),因为工具链提示:
Redundant dependsOn edges between Kotlin Source Sets found.
* ohosArm64Main.dependsOn(concurrentMain)
这个提示具有误导性。它抱怨的是"重复挂 concurrentMain"(模板已经挂过了),
不代表 linuxMain 也挂好了。删掉后立刻报错:
e: ApolloAdaptableWith.kt:6:1 Expected ApolloAdaptableWith has no actual declaration in module <commonMain> for Native
而该 actual 明明存在于 linuxMain。结论:
OHOS 工具链的默认层级模板只把
ohosArm64Main挂到concurrentMain(native 通用),
不会挂linuxMain。
所以必须显式挂上(鸿蒙同为 Linux 内核,POSIX 能力一致):
@OptIn(ExperimentalKotlinGradlePluginApi::class)
private fun KotlinMultiplatformExtension.configureOhosHierarchy() {
if (targets.findByName("ohosArm64") == null) return
val ohosMain = sourceSets.findByName("ohosArm64Main") ?: return
val linuxMain = sourceSets.findByName("linuxMain")
if (linuxMain != null) {
if (!ohosMain.dependsOn.contains(linuxMain)) {
ohosMain.dependsOn(linuxMain)
}
} else {
// ohosOnly 模式下没有 linux 目标,直接把源码目录并进来
val dir = project.file("src/linuxMain/kotlin")
if (dir.isDirectory) {
ohosMain.kotlin.srcDir(dir)
}
}
}
挂了之后,ohosArm64Main 里就不要再重复写 linuxMain 已有的 actual,否则 Conflicting overloads。
我最初在 apollo-api 里手抄了 toJson.kt 和 OhosApolloUnsafeCast.kt,
后来全部删掉——它们已由 linuxMain 提供。
3.3 依赖升级(最高频的坑)
编译时报:
Could not resolve com.squareup.okio:okio:3.9.0.
> No matching variant of com.squareup.okio:okio:3.9.0 was found.
The consumer ... attribute 'org.jetbrains.kotlin.native.target' with value 'ohos_arm64'
原因:传递依赖也没有鸿蒙变体。Apollo 依赖的 okio / uuid / coroutines / atomicfu 都得换成
CPF 已适配的定制版本:
atomicfu = "0.31.0-1.0.0"
kotlinx-coroutines = "1.10.2-1.0.0"
kotlinx-datetime = "0.7.1-1.0.0"
okio = "3.16.4-1.0.0"
uuid = { group = "com.benasher44", name = "uuid", version = "0.8.4-1.0.0" }
通用做法:把
libraries.toml里所有版本号搜一遍,能升的全升到 CPF 的-1.0.0线。
但有一个例外:build-logic/ 是 Gradle 的 included build,用 embedded Kotlin 2.0 编译。
如果把 kotlinx-serialization 升到 1.9.1,它的元数据版本是 2.2.0,build-logic 直接编译失败:
e: kotlin-stdlib-2.2.21-0.3.0.jar!/META-INF/kotlin-stdlib.kotlin_module
Module was compiled with an incompatible version of Kotlin.
The binary version of its metadata is 2.2.0, expected version is 2.0.0.
所以 kotlinx-serialization-runtime 保持 1.6.2 不动
(确认过只有 apollo-ast 用它,而 apollo-ast 是构建期工具,不需要 ohos)。
3.4 关闭 -Werror
升依赖引入了上游的 deprecation 警告,而 Apollo 的构建约定里开了 allWarningsAsErrors(true):
e: warnings found and -Werror specified
w: KotlinxTimeAdapters.kt:8:8 'class Instant ...' is deprecated. Use kotlin.time.Instant instead.
适配构建不追求零警告,关掉:
// build-logic/src/main/kotlin/CompilerOptions.kt
allWarningsAsErrors(false) // was true
3.5 手写 ohos actual(6 个文件)
这是真正的"平台实现"工作。逐模块清点 expect 后逐个补:
| 模块 | 文件 | 实现要点 |
|---|---|---|
apollo-mpp-utils | utils.kt | currentTimeMillis() 用 POSIX clock_gettime(CLOCK_REALTIME) |
apollo-adapters | BigDecimal.kt | 纯 Kotlin 十进制实现(鸿蒙无 NSDecimalNumber/java.math.BigDecimal) |
apollo-normalized-cache | Lock.kt | AtomicFu ReentrantLock(照抄 appleMain) |
apollo-normalized-cache-api | -cache-lock-ohos.kt | 同上 |
apollo-runtime | DefaultHttpEngine.ohos.kt | ktor-client-cio 引擎 |
apollo-runtime | WebSocketEngine.ohos.kt ×2 | ktor WebSockets 插件(新旧两套 API) |
BigDecimal 是最费心的一个,因为鸿蒙没有任何现成的大数实现:
actual class BigDecimal internal constructor(
private val unscaled: Long, // 去掉小数点后的整数
private val scale: Int, // 小数位数
) : Number() {
actual fun add(augend: BigDecimal): BigDecimal {
val s = maxOf(scale, augend.scale)
return BigDecimal(rescaledTo(s) + augend.rescaledTo(s), s)
}
actual fun multiply(multiplicand: BigDecimal): BigDecimal =
BigDecimal(unscaled * multiplicand.unscaled, scale + multiplicand.scale)
// ...
}
3.6 手写 HTTP / WebSocket 引擎(最实质的适配)
Apollo 的 native 侧只有 Apple 实现(appleMain),apollo-runtime 连 linuxMain 都没有。
所以鸿蒙的 HTTP / WebSocket 必须新写。这里复用 CPF 已适配的 ktor:
// apollo-runtime/src/ohosArm64Main/.../DefaultHttpEngine.ohos.kt
actual fun DefaultHttpEngine(timeoutMillis: Long): HttpEngine = OhosHttpEngine(timeoutMillis)
private class OhosHttpEngine(private val timeoutMillis: Long) : HttpEngine {
private val client: HttpClient by lazy {
HttpClient(CIO) {
engine { requestTimeout = timeoutMillis }
expectSuccess = false
}
}
override suspend fun execute(request: HttpRequest): HttpResponse {
val ktorResponse = client.request(request.url) {
method = when (request.method) {
HttpMethod.Get -> KtorHttpMethod.Get
HttpMethod.Post -> KtorHttpMethod.Post
}
request.headers.forEach { headers.append(it.name, it.value) }
request.body?.let { body ->
val buffer = Buffer()
body.writeTo(buffer)
setBody(ByteArrayContent(buffer.readByteArray(), ContentType.parse(body.contentType)))
}
}
val builder = HttpResponse.Builder(ktorResponse.status.value)
ktorResponse.headers.entries().forEach { e ->
e.value.forEach { builder.addHeader(e.key, it) }
}
return builder.body(Buffer().write(ktorResponse.readRawBytes())).build()
}
override fun close() { client.close() }
}
WebSocket 部分要点类似,但有两个 ktor API 细节必须注意:
// ① 取响应体用 readRawBytes()(旧名 readBytes 已废弃)
val bytes = ktorResponse.readRawBytes()
// ② webSocketSession 的 url 要用 lambda 形式
client.webSocketSession {
url { takeFrom(newUrl) }
extraHeaders.forEach { header -> this.headers.append(header.name, header.value) }
}
第 ② 点踩过坑:函数参数名 headers 与 HttpRequestBuilder.headers 同名,
lambda 内 headers.append(...) 被解析成了 StringBuilder.append,报一堆候选不匹配。
解决办法是局部改名 + 显式 this。
另外 ktor 版本要单独处理:Apollo 4.3.0 自带 ktor 3.0.0(无 ohos 变体),
而 CPF 适配的是 3.3.3-1.0.0。由于只在 ohosArm64Main 声明,不影响其他平台:
findByName("ohosArm64Main")?.apply {
dependencies {
implementation("io.ktor:ktor-client-core:3.3.3-1.0.0")
implementation("io.ktor:ktor-client-cio:3.3.3-1.0.0")
implementation("io.ktor:ktor-client-websockets:3.3.3-1.0.0")
}
}
四、验证
# 7 个模块全部编译通过
gradlew :apollo-annotations:compileKotlinOhosArm64 \
:apollo-mpp-utils:compileKotlinOhosArm64 \
:apollo-api:compileKotlinOhosArm64 \
:apollo-adapters:compileKotlinOhosArm64 \
:apollo-normalized-cache-api:compileKotlinOhosArm64 \
:apollo-normalized-cache:compileKotlinOhosArm64 \
:apollo-runtime:compileKotlinOhosArm64
结果:
> Task :apollo-annotations:compileKotlinOhosArm64
> Task :apollo-mpp-utils:compileKotlinOhosArm64
> Task :apollo-api:compileKotlinOhosArm64
> Task :apollo-normalized-cache-api:compileKotlinOhosArm64
> Task :apollo-adapters:compileKotlinOhosArm64
> Task :apollo-normalized-cache:compileKotlinOhosArm64
> Task :apollo-runtime:compileKotlinOhosArm64
BUILD SUCCESSFUL

五、小结
| 改动类别 | 文件数 | 说明 |
|---|---|---|
| 构建脚本 | 4 | 版本、仓库、移除 BCV、注册目标 |
| 模块构建脚本 | 4 | jvmTest 安全引用、wasmJsMain!! 改 ?. |
| 新增 ohos actual | 6 | mpp-utils / adapters / cache / cache-api / runtime |
| 新增 ohos 引擎 | 3 | HTTP ×1、WebSocket ×2 |
三条最重要的经验:
- 先查工具链版本上限,再决定上游库版本——顺序反了会白做
linuxMain不会自动继承,必须显式dependsOn- 传递依赖也要有 ohos 变体,否则换 CPF 定制版
下一篇讲如何在鸿蒙工程里使用 Apollo。
更多推荐




所有评论(0)