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.20gradle/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-validator0.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 上不可构建)valohosOnly=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)privatefunKotlinMultiplatformExtension.configureOhosHierarchy(){if(targets.findByName("ohosArm64")==null)returnvalohosMain=sourceSets.findByName("ohosArm64Main")?:returnvallinuxMain=sourceSets.findByName("linuxMain")if(linuxMain!=null){if(!ohosMain.dependsOn.contains(linuxMain)){ohosMain.dependsOn(linuxMain)}}else{// ohosOnly 模式下没有 linux 目标,直接把源码目录并进来valdir=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.ktallWarningsAsErrors(false)// was true3.5 手写 ohos actual(6 个文件)
这是真正的"平台实现"工作。逐模块清点expect后逐个补:
| 模块 | 文件 | 实现要点 |
|---|---|---|
apollo-mpp-utils | utils.kt | currentTimeMillis()用 POSIXclock_gettime(CLOCK_REALTIME) |
apollo-adapters | BigDecimal.kt | 纯 Kotlin 十进制实现(鸿蒙无NSDecimalNumber/java.math.BigDecimal) |
apollo-normalized-cache | Lock.kt | AtomicFuReentrantLock(照抄 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是最费心的一个,因为鸿蒙没有任何现成的大数实现:
actualclassBigDecimalinternalconstructor(privatevalunscaled:Long,// 去掉小数点后的整数privatevalscale:Int,// 小数位数):Number(){actualfunadd(augend:BigDecimal):BigDecimal{vals=maxOf(scale,augend.scale)returnBigDecimal(rescaledTo(s)+augend.rescaledTo(s),s)}actualfunmultiply(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.ktactualfunDefaultHttpEngine(timeoutMillis:Long):HttpEngine=OhosHttpEngine(timeoutMillis)privateclassOhosHttpEngine(privatevaltimeoutMillis:Long):HttpEngine{privatevalclient:HttpClientbylazy{HttpClient(CIO){engine{requestTimeout=timeoutMillis}expectSuccess=false}}overridesuspendfunexecute(request:HttpRequest):HttpResponse{valktorResponse=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->valbuffer=Buffer()body.writeTo(buffer)setBody(ByteArrayContent(buffer.readByteArray(),ContentType.parse(body.contentType)))}}valbuilder=HttpResponse.Builder(ktorResponse.status.value)ktorResponse.headers.entries().forEach{e->e.value.forEach{builder.addHeader(e.key,it)}}returnbuilder.body(Buffer().write(ktorResponse.readRawBytes())).build()}overridefunclose(){client.close()}}WebSocket 部分要点类似,但有两个 ktor API 细节必须注意:
// ① 取响应体用 readRawBytes()(旧名 readBytes 已废弃)valbytes=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。