由浅入深,从基本概念到源码原理,再到实战示例与生产级应用案例,系统梳理 Kotlin 跨平台开发之道
一、基础概念
Kotlin Multiplatform(又称 KMP 或 KMM)是 JetBrains 推出的跨平台代码共享方案,其核心理念是:共享业务逻辑,保留原生 UI。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ┌─────────────────────────────────────────────────────────────────────────┐ │ KMP 架构:共享逻辑,原生 UI │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │ │ │ Android │ │ iOS │ │ Web │ │ │ │ (Kotlin) │ │ (Swift) │ │ (JS/WasM) │ │ │ └──────┬──────┘ └──────┬──────┘ └──────┬──────┘ │ │ │ │ │ │ │ │ 原生 UI 层 │ │ │ │ ▼ ▼ ▼ │ │ ┌──────────────────────────────────────────────────────────────┐ │ │ │ 共享模块 (shared / commonMain) │ │ │ │ 业务逻辑 · 网络请求 · 数据模型 · 存储 · ViewModel │ │ │ └──────────────────────────────────────────────────────────────┘ │ │ │ └─────────────────────────────────────────────────────────────────────────┘
|
与 Flutter、React Native 等「一套 UI 到处跑」的方案不同,KMP 让你:
- 用 Kotlin 写共享的业务逻辑、网络、数据层
- 用 Jetpack Compose 写 Android UI,用 SwiftUI 写 iOS UI
- 得到的是 原生体验,而非 WebView 或自绘引擎
1.2 为什么需要 KMP?
| 痛点 |
KMP 的解决方式 |
| Android / iOS 重复实现业务逻辑 |
共享 ViewModel、UseCase、Repository 等 |
| 双端行为不一致(如计算逻辑) |
同一套 Kotlin 代码,编译到各平台 |
| 双端各自维护一套网络/序列化代码 |
共享 Ktor + kotlinx.serialization |
| 希望跨平台但不牺牲原生体验 |
保留原生 UI 框架 |
| 团队已有 Kotlin 基础 |
复用技能栈,降低学习成本 |
1.3 KMP 与主流跨平台方案对比
| 维度 |
KMP |
Flutter |
React Native |
| UI 方案 |
原生 UI(Compose / SwiftUI) |
自绘引擎(Skia) |
原生组件 + Bridge |
| 共享范围 |
业务逻辑、网络、数据 |
UI + 逻辑全部共享 |
UI + 逻辑共享 |
| 性能 |
接近原生 |
接近原生 |
依赖 JS Bridge |
| 包体积 |
共享库较小 |
需携带 Flutter 引擎 |
需携带 RN 运行时 |
| 生态 |
Kotlin 生态、官方支持 |
Dart/Flutter 生态 |
JS/React 生态 |
| 学习曲线 |
Kotlin 开发者友好 |
需学 Dart |
需学 React/JS |
适用场景:KMP 更适合「业务逻辑复杂、希望 UI 保持原生」的应用,如金融、电商、工具类 App。
二、核心原理
2.1 编译模型
KMP 将 Kotlin 代码编译成各平台的原生产物,而不是通过虚拟机或解释器运行:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| ┌──────────────────┐ │ commonMain │ │ (共享 Kotlin) │ └────────┬─────────┘ │ ┌───────────────────┼───────────────────┐ ▼ ▼ ▼ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │ JVM / Android │ │ Kotlin/Native │ │ Kotlin/JS │ │ (.class/DEX) │ │ (LLVM → .a) │ │ (JS/Wasm) │ └────────────────┘ └────────────────┘ └────────────────┘ │ │ │ ▼ ▼ ▼ Android APK iOS Framework Web Bundle
|
- Android:Kotlin → JVM 字节码 → DEX → APK
- iOS:Kotlin → Kotlin/Native(LLVM)→ 静态库/框架
- Web:Kotlin → JavaScript 或 WebAssembly
因此,共享代码在运行时就是本地代码,无额外解释层。
2.2 expect / actual 机制
当共享代码需要调用平台特有 API 时(如文件系统、UUID、日期格式化),KMP 使用 expect / actual 做编译期抽象:
在 commonMain 中声明 expect(契约):
1 2 3 4 5
| package platform
expect fun currentTimeMillis(): Long expect fun randomUUID(): String
|
在各平台提供 actual 实现:
1 2 3 4 5 6 7
| package platform
import java.util.UUID
actual fun currentTimeMillis(): Long = System.currentTimeMillis() actual fun randomUUID(): String = UUID.randomUUID().toString()
|
1 2 3 4 5 6 7 8
| package platform
import platform.Foundation.NSDate import platform.Foundation.NSUUID
actual fun currentTimeMillis(): Long = (NSDate().timeIntervalSince1970 * 1000).toLong() actual fun randomUUID(): String = NSUUID().UUIDString()
|
原理要点:
- 编译时,编译器将
expect 与对应平台的 actual 匹配
- 每个目标平台都必须有且仅有一个
actual 实现
- 保证 common 代码只能依赖抽象,无法引用平台专属 API
2.3 Source Sets(源集)与层级结构
KMP 用 Source Set 组织代码,每个源集对应一组目标平台:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ┌─────────────────────────────────────────────────────────────────┐ │ Source Sets 层级 │ ├─────────────────────────────────────────────────────────────────┤ │ │ │ commonMain ◄── 编译到所有目标,只可写平台无关代码 │ │ │ │ │ ├── androidMain ◄── 仅 Android │ │ │ │ │ ├── iosMain (中间源集) ◄── 所有 iOS 目标共享 │ │ │ ├── iosArm64Main (真机) │ │ │ └── iosSimulatorArm64Main (模拟器) │ │ │ │ │ └── appleMain (中间源集) ◄── iOS + macOS + watchOS + tvOS │ │ │ └─────────────────────────────────────────────────────────────────┘
|
依赖方向:androidMain → commonMain,iosMain → commonMain,commonMain 不能依赖平台源集。
典型目录结构:
1 2 3 4 5 6 7 8 9 10 11 12
| shared/ ├── src/ │ ├── commonMain/ │ │ └── kotlin/ │ │ ├── domain/ │ │ ├── data/ │ │ └── di/ │ ├── androidMain/ │ │ └── kotlin/ │ └── iosMain/ │ └── kotlin/ └── build.gradle.kts
|
三、源码与实现原理
3.1 expect / actual 的编译期处理
expect/actual 并非运行时多态,而是编译期替换:
- 在 common 编译时,
expect 仅作为「占位声明」参与类型检查
- 在平台编译时,编译器用对应平台的
actual 替换 expect
- 最终产物中只存在
actual 实现,无额外抽象开销
这保证了共享代码在目标平台上等价于「直接调用平台 API」。
3.2 中间源集(Hierarchical Source Sets)
当多个平台共享同一套「非 common」逻辑时,可引入中间源集,避免重复:
1 2 3 4 5 6 7
| kotlin { android() iosArm64() iosSimulatorArm64() macosArm64() }
|
Kotlin 插件会自动生成:
appleMain:所有 Apple 平台共享(iOS + macOS + watchOS + tvOS)
iosMain:iOS 真机 + 模拟器共享
在 appleMain 中可以使用 Apple 专属 API(如 platform.Foundation.NSUUID),而无需在 iosArm64Main、iosSimulatorArm64Main 里各写一遍。
3.3 依赖解析规则
1 2 3 4 5 6 7 8 9 10 11 12 13
| commonMain 只能依赖: - Kotlin 标准库(多平台版本) - kotlinx-* 多平台库(coroutines, serialization, datetime 等) - 其他 commonMain 或「包含当前目标」的中间源集
androidMain 可额外依赖: - Android SDK - Jetpack 库 - 纯 JVM 库
iosMain 可额外依赖: - Kotlin/Native 平台库 - CocoaPods 依赖
|
四、实战示例
4.1 项目搭建
根 build.gradle.kts:
1 2 3 4 5
| plugins { kotlin("multiplatform") version "2.0.0" apply false kotlin("plugin.serialization") version "2.0.0" apply false id("com.android.application") version "8.2.0" apply false }
|
shared/build.gradle.kts(共享模块):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| plugins { kotlin("multiplatform") kotlin("plugin.serialization") }
kotlin { androidTarget() listOf( iosX64(), iosArm64(), iosSimulatorArm64() ).forEach { iosTarget -> iosTarget.binaries.framework { baseName = "shared" isStatic = true } }
sourceSets { commonMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") implementation("io.ktor:ktor-client-core:2.3.6") implementation("io.ktor:ktor-client-content-negotiation:2.3.6") implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.6") } androidMain.dependencies { implementation("io.ktor:ktor-client-okhttp:2.3.6") } iosMain.dependencies { implementation("io.ktor:ktor-client-darwin:2.3.6") } } }
|
4.2 网络层:Ktor + kotlinx.serialization
commonMain - 数据模型与 API:
1 2 3 4 5 6 7 8 9 10 11 12
| import kotlinx.serialization.Serializable
@Serializable data class User( val id: Long, val name: String, val email: String )
@Serializable data class ApiResponse<T>(val data: T)
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| import io.ktor.client.* import io.ktor.client.call.* import io.ktor.client.request.* import io.ktor.http.*
class UserApi(private val client: HttpClient) { suspend fun getUser(id: Long): User { return client.get("https://api.example.com/users/$id") { contentType(ContentType.Application.Json) }.body() } }
|
commonMain - Ktor 客户端工厂:
1 2 3 4 5 6 7 8 9 10 11 12
| import io.ktor.client.* import io.ktor.client.plugins.contentnegotiation.* import io.ktor.serialization.kotlinx.json.* import kotlinx.serialization.json.Json
expect fun createHttpClient(): HttpClient
fun createJson(): Json = Json { ignoreUnknownKeys = true isLenient = true }
|
androidMain - OkHttp 引擎:
1 2 3 4 5 6 7 8 9 10
| import io.ktor.client.* import io.ktor.client.engine.okhttp.* import io.ktor.client.plugins.contentnegotiation.*
actual fun createHttpClient(): HttpClient = HttpClient(OkHttp) { install(ContentNegotiation) { json(createJson()) } }
|
iosMain - Darwin 引擎:
1 2 3 4 5 6 7 8 9 10
| import io.ktor.client.* import io.ktor.client.engine.darwin.* import io.ktor.client.plugins.contentnegotiation.*
actual fun createHttpClient(): HttpClient = HttpClient(Darwin) { install(ContentNegotiation) { json(createJson()) } }
|
4.3 ViewModel 共享
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import kotlinx.coroutines.flow.* import kotlinx.coroutines.*
class UserViewModel( private val userApi: UserApi, private val scope: CoroutineScope ) { private val _user = MutableStateFlow<User?>(null) val user: StateFlow<User?> = _user.asStateFlow()
fun loadUser(id: Long) { scope.launch { _user.value = userApi.getUser(id) } } }
|
- Android:注入
viewModelScope,通过 viewModel() 获取
- iOS:注入
MainScope() 或 ViewModelScope,通过 KMP 的 StateFlow 订阅
两端共享同一套 ViewModel 逻辑,仅 CoroutineScope 由各平台注入。
4.4 expect/actual 实用示例:日期格式化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| expect fun formatDate(timestamp: Long): String
actual fun formatDate(timestamp: Long): String { val sdf = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) return sdf.format(Date(timestamp)) }
actual fun formatDate(timestamp: Long): String { val formatter = NSDateFormatter().apply { dateFormat = "yyyy-MM-dd" } return formatter.stringFromDate(NSDate(timestamp = timestamp / 1000.0)) }
|
五、实际项目应用案例
5.1 Cash App(Block 旗下金融应用)
| 项目规模 |
50+ 移动工程师,约 3000 万月活 |
| 策略 |
业务共享、UI 原生 |
| 时间线 |
自 2018 年起试验 KMP,逐步通过 Feature Flag 落地 |
| 效果 |
移除有问题的 JavaScript 共享代码;双端维护单一业务逻辑库;服务器团队(Kotlin)可直接参与共享库开发 |
| 使用库 |
SQLDelight、Wire、CrashKiOS |
启示:大型金融场景下,KMP 在「持久化」和「纯函数业务逻辑」上收益最大,且便于服务端参与共享代码。
5.2 Netflix
- 在移动工作室 App 中共享逻辑,减少重复开发
- 在影视制作的高节奏迭代下,提升交付效率与稳定性
5.3 McDonald’s
- 共享应用内支付等复杂业务逻辑
- 支撑每月 650 万+ 笔支付
- 验证 KMP 在电商/支付场景的可行性
5.4 Forbes
- 在 iOS 与 Android 间共享 80%+ 逻辑
- 双端可同步上线新功能,缩短发布周期
5.5 其他采用者
- Wrike、Bilibili、Feres 等使用 KMP + Compose Multiplatform
- Vouched、Karma 等采用 KMP 共享核心业务
六、最佳实践与注意事项
6.1 共享范围建议
| 适合共享 |
不建议共享 |
| 数据模型、DTO |
平台特有 UI 组件 |
| 网络请求、序列化 |
复杂平台动画、手势 |
| Repository、UseCase、ViewModel |
平台特定系统 API 封装 |
| 业务规则、校验逻辑 |
第三方 SDK 深度集成 |
| 本地存储(SQLDelight、DataStore) |
推送、支付等强平台相关逻辑 |
6.2 常见陷阱
- 在 commonMain 中引用平台 API:编译器会报错,应使用 expect/actual 抽象
- expect/actual 签名不一致:必须保持完全一致(包名、函数名、参数、返回值)
- 并发与线程:Kotlin/Native 对线程模型有约束,需注意
@ThreadLocal、freeze 等
- 依赖版本:多平台库需保证各目标使用兼容版本
6.3 学习路线建议
- 搭建最小 KMP 工程,跑通 Android + iOS
- 用 expect/actual 封装 1~2 个平台 API
- 接入 Ktor + kotlinx.serialization 实现共享网络层
- 共享一个 ViewModel,在双端展示数据
- 按业务模块逐步迁移,避免一次性大改
七、小结
KMP 以 「共享逻辑、原生 UI」 为核心,通过 expect/actual 和 Source Sets 实现跨平台抽象,在不牺牲原生体验的前提下显著降低双端重复开发。从 Cash App、Netflix、McDonald’s 等实践来看,KMP 已可用于生产环境,特别适合业务逻辑复杂、对一致性与性能要求较高的应用。结合 Jetpack Compose 与 SwiftUI,KMP 正成为 Android 开发者拓展 iOS 能力的重要路径。
参考资料