Kotlin Multiplatform 如何同时支持 Android 与 iOS:从 commonMain 到 XCFramework
Kotlin Multiplatform 如何同时支持 Android 与 iOS:从 commonMain 到 XCFramework
开篇:共享代码能编译,不等于两端都能消费
跨平台应援灯牌控制应用同时面向 Android 和 iOS 用户,同一条业务规则如果分别写在 Kotlin、Swift 和 Flutter 中,时间久了就会出现输入处理不一致、错误文案不一致和修复遗漏。Kotlin Multiplatform 的价值,是把适合共享的规则放进 commonMain,再让各平台通过自己的交付方式消费它。
Kotlin Multiplatform 的难点通常不在 commonMain 里写出一个类,而在于共享代码如何进入 Android 应用、如何生成 iOS 能够链接的框架,以及框架还没有接入时应用怎样给出可诊断的结果。
本文以跨平台应援灯牌控制应用的 Flutter + KMP + OpenHarmony 多端工程为背景。当前示例中的 shared 模块提供了一个最小但完整的路径:commonMain 定义问候规则,commonTest 验证规则,Android 以项目依赖消费,iOS 通过 Shared.xcframework 接入。本文只讨论共享模块的交付边界,不重复介绍 Flutter 页面布局。读完本文,你会知道一段共享规则从源码到 Android 依赖、再到 iOS XCFramework 的完整路径,也能判断问题发生在业务代码、框架产物还是宿主链接阶段。

文章目标和工程边界
本文会回答四个问题:
- commonMain 代码应该放什么。
- Android target 和 iOS target 如何声明。
- Android 项目依赖与 iOS XCFramework 的接入方式有什么不同。
- 共享框架未链接时,Flutter 如何收到明确错误。

1. 先看 shared 模块的消费关系
Android Settings 中把 shared 纳入同一个 Gradle 工程:
include(":app")
include(":shared")
project(":shared").projectDir = file("../shared")
应用模块再通过项目依赖消费:
dependencies {
implementation(project(":shared"))
}
Android 这条路径不需要复制源码,也不需要把 KMP 类手动导出成普通 JAR。Gradle 负责把 shared 的 Android target 编译进应用。源码位置是 android/settings.gradle.kts 和 android/app/build.gradle.kts。
2. commonMain 只放可共享规则
共享服务的实现保持平台无关:
package com.example.lesson59.shared
class GreetingService {
@Throws(IllegalArgumentException::class)
fun greet(name: String): String {
val normalizedName = name.trim()
require(normalizedName.isNotEmpty()) { "Name must not be blank." }
return "你好,$normalizedName!这条消息来自 KMP 共享模块。"
}
}
这个类没有 Android Context、iOS UIKit 或 Flutter 类型,因此可以放进 commonMain。输入校验和返回文案属于业务规则,应该只维护一份;平台层只负责调用和错误翻译。
3. Gradle target 决定可交付平台
当前 shared/build.gradle.kts 声明了 Android、JVM 和三个 iOS target:
kotlin {
androidTarget {
compilations.configureEach {
compileTaskProvider.configure {
compilerOptions.jvmTarget.set(JvmTarget.JVM_11)
}
}
}
jvm()
val xcFramework = XCFramework("Shared")
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64(),
).forEach { target ->
target.binaries.framework {
baseName = "Shared"
isStatic = true
xcFramework.add(this)
}
}
}
Android target 解决宿主依赖,JVM target 方便本地测试,iOS 三个 target 覆盖 Intel 模拟器、真机和 Apple Silicon 模拟器。XCFramework 把这些二进制合并为一个 iOS 可消费的交付物。这个配置的输入是 Kotlin 编译目标,输出是不同平台的库产物。

4. commonTest 验证共享规则
共享规则应该在进入平台之前先验证:
class GreetingServiceTest {
private val service = GreetingService()
@Test
fun trimsNameAndReturnsGreeting() {
assertEquals(
"你好,Flutter!这条消息来自 KMP 共享模块。",
service.greet(" Flutter "),
)
}
@Test
fun rejectsBlankName() {
assertFailsWith<IllegalArgumentException> {
service.greet(" ")
}
}
}
测试覆盖了两条边界:带空格的正常输入会被规整,只有空格的输入会失败。它不能证明 Flutter 通道、Android Activity 或 iOS 框架链接成功,但能确保平台层拿到的是一套确定的业务规则。
5. Android 通过项目依赖进入 KMP
Android 宿主在 MainActivity 创建 GreetingService:
class MainActivity : FlutterActivity() {
private val greetingService = GreetingService()
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channelName)
.setMethodCallHandler { call, result ->
when (call.method) {
"getGreeting" -> {
val name = call.argument<String>("name").orEmpty()
runCatching { greetingService.greet(name) }
.onSuccess(result::success)
.onFailure {
result.error("INVALID_NAME", it.message, null)
}
}
else -> result.notImplemented()
}
}
}
}
MainActivity 只做两件事:把 Flutter 消息交给 shared,把 Kotlin 异常转换成 Flutter 错误。它没有重新实现 trim 和空值判断,因此避免了 Android 规则偏离 commonMain。
6. iOS 通过 XCFramework 接入
项目的 shared/README.md 给出了 iOS 产物命令:
./gradlew :shared:assembleSharedDebugXCFramework
产物路径为:
build/shared/XCFrameworks/debug/Shared.xcframework
生成框架后,需要把它加入 Runner 的 Frameworks, Libraries, and Embedded Content。iOS 的 AppDelegate.swift 使用条件编译,因此源码在框架尚未链接时仍然可以保留清晰的错误分支,而不是直接假设所有开发机都有框架。
7. canImport 保护了什么
#if canImport(Shared)
import Shared
#endif
当框架存在时,Swift 可以调用 GreetingService().greet(name:);当框架不存在时,代码进入 KMP_UNAVAILABLE 分支:
result(FlutterError(
code: "KMP_UNAVAILABLE",
message: "Shared.framework has not been linked.",
details: nil
))
这个错误码把“业务输入失败”和“框架接入缺失”分开,给 CI 和开发者留下了明确排查入口。
8. 版本边界
| 配置 | 当前值 |
|---|---|
| Dart SDK | >=3.9.2 <4.0.0 |
| Flutter | 本地工具为 3.35.8-ohos-1.0.3-beta |
| Android Gradle Plugin | 8.9.1 |
| Kotlin | 2.3.10 |
| Android JVM | 11 |
| iOS 交付物 | Shared.xcframework |
Kotlin、Gradle、Xcode 和 Flutter 版本变化时,优先检查 target、框架任务和宿主链接,不要先修改业务类。
9. KMP 交付的故障排查
Android:
cd D:\lesson\lesson59\android
.\gradlew.bat :shared:jvmTest :shared:assembleDebug
iOS:
cd android
./gradlew :shared:assembleSharedDebugXCFramework
| 问题 | 可能原因 | 检查方式 |
|---|---|---|
| shared 找不到 | Settings 没有 include | 检查 settings.gradle.kts |
| JVM target 不一致 | Kotlin 编译目标未统一 | 查看 Android 与 KMP compilerOptions |
| iOS 找不到 Shared | 框架未生成或未加入 Runner | 检查 XCFramework 路径和 Xcode 链接项 |
| Swift 调用参数报错 | KMP 导出签名变化 | 对照生成框架中的 Swift API |
当前 Flutter analyze 和 Flutter test 已通过;KMP Gradle 命令和 iOS 框架构建需要对应工具链,仍需在相应环境中执行。
10. 共享模块的验收清单
- 业务规则只在 commonMain 维护一份。
- Android 通过 implementation(project(“:shared”)) 消费模块。
- iOS target 覆盖真机和两类模拟器。
- commonTest 覆盖正常输入和非法输入。
- iOS 框架未接入时能返回 KMP_UNAVAILABLE。
- 平台层只做桥接,不重复业务校验。
共享模块的交付边界
KMP 的交付边界可以按三层理解:commonMain 保存业务规则,Gradle target 负责生成平台产物,Android/iOS 宿主负责把产物接入 Flutter。把这三层分开后,共享代码是否可用、平台链接是否成功、Flutter 通道是否正常就能分别验证。
因此,发布记录最好同时保留 commonTest 结果、Gradle 任务结果和宿主启动结果。只有三者都具备,才可以把“共享模块已交付”写成完整结论。
更多推荐



所有评论(0)