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
工具链插件源码 / Issuehttps://gitcode.com/CPF-KMP-CMP/ohos-kmp-toolchain
文档总目录CPF 文档 · 目录

文档仓库的分支名带版本号(当前 main-2.2,对应 KMP 2.2.21 / CMP 1.9.2)。
工具链换代后分支与链接都会变,上面的链接若 404,从 CPF 组织首页 进 docs 仓库,切到与工具链版本匹配的分支。

0.2 环境版本(本文验证所用)

项版本 / 路径说明
JDK17Android Studio 的 Gradle JDK 必须切到 17
Android Studio2024.2.1+KMP 工程侧开发、Gradle Sync
DevEco Studio6.0.0+,本文验证 6.1.0.850鸿蒙壳工程 harmonyApp/、签名、hvigor 打包
HarmonyOS SDKAPI 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(详见 适配问题与解决方案.md F2)。

0.3 上游地址

项地址
上游仓库https://github.com/apollographql/apollo-kotlin
本文所基于的 tagv4.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 实现。

所以"适配"就三件事:

① 注册目标
ohosArm64()

② 补 actual
expect 的鸿蒙实现

③ 出产物
klib / .so

二、第一道门槛:版本选型

这是最容易忽略、也最容易卡死的一步。鸿蒙定制 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.02.3.10✗ 超出上限
4.3.02.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 路径。

sysroot目录

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-utilsutils.ktcurrentTimeMillis() 用 POSIX clock_gettime(CLOCK_REALTIME)
apollo-adaptersBigDecimal.kt纯 Kotlin 十进制实现(鸿蒙无 NSDecimalNumber/java.math.BigDecimal)
apollo-normalized-cacheLock.ktAtomicFu ReentrantLock(照抄 appleMain)
apollo-normalized-cache-api-cache-lock-ohos.kt同上
apollo-runtimeDefaultHttpEngine.ohos.ktktor-client-cio 引擎
apollo-runtimeWebSocketEngine.ohos.kt ×2ktor 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、注册目标
模块构建脚本4jvmTest 安全引用、wasmJsMain!! 改 ?.
新增 ohos actual6mpp-utils / adapters / cache / cache-api / runtime
新增 ohos 引擎3HTTP ×1、WebSocket ×2

三条最重要的经验:

  1. 先查工具链版本上限,再决定上游库版本——顺序反了会白做
  2. linuxMain 不会自动继承,必须显式 dependsOn
  3. 传递依赖也要有 ohos 变体,否则换 CPF 定制版

下一篇讲如何在鸿蒙工程里使用 Apollo。

Logo

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

更多推荐