26、Kotlin Multiplatform:KMP项目结构、Gradle配置、平台源码集、依赖管理
Kotlin Multiplatform(简称 KMP)是 JetBrains 推出的一套跨平台方案。说实话,它跟 Flutter、React Native 不太一样。KMP 不是要你写一套 UI 跑所有平台,而是让你共享业务逻辑层——网络请求、数据校验、状态管理这些。UI 层你该怎么写还怎么写,用原生控件。
我个人觉得,KMP 最大的价值在于:你可以在不牺牲原生体验的前提下,把那些「脏活累活」统一掉。我在项目中遇到过好几次,iOS 和 Android 两边的网络层各写各的,接口字段解析不一致,bug 还互相传染。用 KMP 之后,这种问题基本绝迹了。
KMP 项目结构长什么样?
一个典型的 KMP 项目,目录结构大概是这样的:
MyKmpProject/
├── shared/
│ ├── src/
│ │ ├── commonMain/ // 共享代码
│ │ ├── androidMain/ // Android 平台实现
│ │ └── iosMain/ // iOS 平台实现
│ └── build.gradle.kts
├── androidApp/
│ └── src/
├── iosApp/
│ └── (Xcode 项目)
└── build.gradle.kts
这里最核心的就是 shared 模块。它下面有三个源码集:
- commonMain:放所有平台通用的代码。比如数据模型、业务逻辑、接口定义。
- androidMain:放 Android 特有的实现。比如用 OkHttp 做网络请求。
- iosMain:放 iOS 特有的实现。比如用 NSURLSession 做网络请求。
你想想看,这种结构天然就逼着你做「面向接口编程」。你在 commonMain 里定义接口,然后在各平台源码集里提供实现。代码干净,职责清晰。
核心原则:commonMain 里不要出现任何平台相关的 API。一旦你写了 android.os.Build 或者 UIKit 的东西,编译直接报错。
Gradle 配置:怎么把 KMP 跑起来?
配置 KMP 的 Gradle 脚本,说简单也简单,说坑也多。我直接给你一个能用的模板,咱们一行一行拆开看。
首先,在根项目的 build.gradle.kts 里声明插件:
plugins {
kotlin("multiplatform") version "1.9.22" apply false
id("com.android.library") version "8.2.0" apply false
}
然后在 shared/build.gradle.kts 里应用插件并配置目标:
plugins {
kotlin("multiplatform")
id("com.android.library")
}
kotlin {
// 配置 Android 目标
androidTarget {
compilations.all {
kotlinOptions {
jvmTarget = "17"
}
}
}
// 配置 iOS 目标
listOf(
iosX64(),
iosArm64(),
iosSimulatorArm64()
).forEach {
it.binaries.framework {
baseName = "shared"
isStatic = true
}
}
// 源码集配置
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.7")
}
}
val iosX64Main by getting
val iosArm64Main by getting
val iosSimulatorArm64Main by getting
val iosMain by creating {
dependsOn(commonMain)
iosX64Main.dependsOn(this)
iosArm64Main.dependsOn(this)
iosSimulatorArm64Main.dependsOn(this)
dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.7")
}
}
}
}
android {
namespace = "com.example.shared"
compileSdk = 34
defaultConfig {
minSdk = 24
}
}
嗯,这里要注意几个点:
- iOS 目标要写全:x64(模拟器)、arm64(真机)、simulatorArm64(Apple Silicon 模拟器)。少一个,你打包的时候可能就找不到 framework。
- iosMain 是手动创建的:KMP 默认不给你生成 iOS 的公共源码集。你得自己建一个
iosMain,然后让三个 iOS 子目标都依赖它。这样你只需要写一份 iOS 平台代码。 - 依赖要分平台:commonMain 里用
ktor-client-core,androidMain 里用ktor-client-okhttp,iosMain 里用ktor-client-darwin。这是 Ktor 的设计哲学——核心库只定义接口,平台库提供实现。
我曾经踩过一个坑:在 iosMain 里忘了加 dependsOn(commonMain),结果 iOS 那边死活找不到 commonMain 里定义的接口。编译报错信息还特别隐晦,查了半天才发现是源码集依赖没配好。
平台源码集:怎么写平台特定代码?
源码集的核心机制是 expect 和 actual 关键字。你在 commonMain 里用 expect 声明一个函数或类,然后在每个平台源码集里用 actual 提供实现。
举个例子,获取当前平台名称:
// 在 commonMain 中
expect fun getPlatformName(): String
// 在 androidMain 中
actual fun getPlatformName(): String = "Android ${Build.VERSION.SDK_INT}"
// 在 iosMain 中
actual fun getPlatformName(): String = "iOS ${UIDevice.currentDevice.systemVersion}"
你看,调用方完全不需要关心平台差异。写业务逻辑的时候,直接调 getPlatformName() 就行。这就是 KMP 的优雅之处。
不过要注意,expect/actual 不是万能的。有些场景下,你更适合用「接口 + 平台工厂」的模式。比如网络客户端:
// commonMain
interface HttpClientFactory {
fun create(): HttpClient
}
expect fun createHttpClientFactory(): HttpClientFactory
// androidMain
actual fun createHttpClientFactory(): HttpClientFactory = AndroidHttpClientFactory()
// iosMain
actual fun createHttpClientFactory(): HttpClientFactory = IosHttpClientFactory()
我个人习惯是:能用 expect/actual 就用,太复杂了再考虑接口模式。expect/actual 的编译期检查更严格,不容易漏实现。
依赖管理:怎么处理第三方库?
KMP 的依赖管理,说白了就是「分平台声明」。同一个库,不同平台可能要用不同的 artifact。
常见的做法是:
- 纯 Kotlin 库(比如 kotlinx-coroutines、kotlinx-serialization):直接放在 commonMain 里,所有平台通用。
- 平台相关库(比如 Ktor 的 engine、SQLDelight 的 driver):分别放在 androidMain 和 iosMain 里。
- Android 专属库(比如 Room、OkHttp):只放在 androidMain 里。
- iOS 专属库(比如 Alamofire):一般通过 CocoaPods 或 Swift Package Manager 集成,KMP 这边用
cinterop调用。
这里我建议你用一个表格来梳理依赖关系,方便团队对齐:
| 库名 | commonMain | androidMain | iosMain |
|---|---|---|---|
| kotlinx-coroutines-core | ✅ | — | — |
| kotlinx-serialization-json | ✅ | — | — |
| ktor-client-core | ✅ | — | — |
| ktor-client-okhttp | — | ✅ | — |
| ktor-client-darwin | — | — | ✅ |
另外,如果你用 Ktor 做网络请求,记得在 commonMain 里只依赖 ktor-client-core,不要直接依赖 engine。engine 的选择是平台源码集的事,业务层不应该知道。
一个小技巧:在 commonMain 里写一个 expect fun httpClient(): HttpClient,然后在各平台返回配置好的 client。这样业务层调用的时候,根本不知道底层用的是 OkHttp 还是 Darwin,耦合度降到最低。
KMP 核心知识体系
下面这张图帮你梳理了 KMP 的整体结构。你可以看到,commonMain 是中心,各平台源码集围绕它提供实现。依赖也分两层:通用依赖在中心,平台依赖在外围。
这张图其实就概括了 KMP 的全部核心:一个中心(commonMain),两个基本点(androidMain、iosMain),外加一套依赖管理规则。你把这个结构记熟了,KMP 就算入门了。
避坑指南:我踩过的几个坑
最后分享几个实战中容易翻车的地方:
- iOS 模拟器和真机的 framework 不通用:你编译的时候要指定架构。我建议在 Gradle 里用
listOf(iosX64(), iosArm64(), iosSimulatorArm64())一次性全配好,省得来回切。 - Ktor 的 engine 别漏了:commonMain 里只加 core,平台源码集里一定要加对应的 engine。漏了的话,运行时会报
NoTransformationException,而且堆栈信息很不友好。 - 序列化注解要加在 commonMain 里:
@Serializable必须写在 commonMain 的数据类上。如果你写在 androidMain 里,iOS 那边根本看不到。 - iOS 端的 framework 名字要跟 Xcode 里一致:
baseName = "shared"这个值,Xcode 引入的时候要用。改名字的话两边都要同步,不然链接失败。
嗯,KMP 这块内容其实不少,但核心就是「分平台、分源码集、分依赖」。你只要把这三个「分」字理解透了,剩下的就是熟练度的问题了。