KMP 解决什么问题:先划清”共享什么”
一个移动应用的代码里,UI 通常只占三四成,剩下的大头是数据模型、网络请求、业务规则、缓存与持久化——这部分在 Android 和 iOS 上往往被两拨人用两种语言写两遍,还总写出行为不一致。Kotlin Multiplatform(KMP)就是冲着这块来的:业务逻辑用 Kotlin 写一份,编译成各平台的产物,UI 默认各端用各端的原生方案。它和 Flutter、React Native 的出发点不同:那两家是连 UI 一起跨端,KMP 的第一卖点是把”逻辑写两遍”这个成本砍掉,同时保留原生 UI 的可能性。
本站之前的 Kotlin 系列都是 Android 单端视角:环境搭建、第一个 Compose 应用、协程与 Flow 基础。好消息是这些积累全部可以搬进 KMP——协程、Flow、序列化这些 Kotlin 功力在共享代码里原样可用。本文补上”多端”这一块:KMP 的定位、两条技术路线、工程结构和 expect/actual 机制。
两条路线:纯 KMP 与 Compose Multiplatform
社区里说”用 Kotlin 跨端”其实指两条不同的路线,先把它们分开,后面的决策才不混乱:
| 维度 | 纯 KMP(共享逻辑) | Compose Multiplatform(逻辑 + UI 全共享) |
|---|---|---|
| 共享范围 | 数据模型、网络、业务规则、存储 | 上述全部,再加 UI 层 |
| 各端 UI | Android 用 Jetpack Compose,iOS 用 SwiftUI,桌面用 Swing/Compose | 全端用 Compose 绘制,不走系统原生控件 |
| iOS 成熟度 | KMP 本体 2023-11 起稳定(Kotlin 1.9.20 宣布) | 2025-05 的 Compose Multiplatform 1.8.0 起 iOS 端稳定,可上架 App Store |
| 团队要求 | 两端各要有原生 UI 能力,Kotlin 核心团队维护公共层 | 主要是 Kotlin/Compose 技能栈,iOS 侧只需发布与原生桥接能力 |
| 适合场景 | 团队已有原生两拨人,痛点是逻辑重复与不一致 | 中小团队想一人马覆盖三端,UI 一致性优先于原生手感 |
| 主要代价 | UI 仍然写两遍 | 放弃系统原生控件的免费更新(如新系统组件自动获得),复杂列表性能要自己调 |
这两条路线不是二选一的宗教站队,可以渐进:先用纯 KMP 把逻辑层统下来,验证收益后,再逐屏把 UI 挪到 Compose Multiplatform。反过来一上来全共享 UI、逻辑却没理清的项目,通常在第一个性能坑就退回去了。
版本与稳定性现状
跨端技术最怕”学的时候还稳定,上线时已翻篇”,所以先把时间线摆出来(2025-06 核实):
| 时间 | 事件 | 意味着什么 |
|---|---|---|
| 2023-11 | Kotlin 1.9.20,JetBrains 宣布 Kotlin Multiplatform 稳定 | 语言层与工具链承诺向后兼容,可以进生产 |
| 2024-05 | Kotlin 2.0.0,K2 编译器默认启用并稳定 | 新编译器前端覆盖 JVM/Native/Wasm/JS 全目标,编译速度与智能转换显著改善 |
| 2025-05 | Compose Multiplatform 1.8.0,iOS 端转稳定 | Compose 写的 iOS UI 可正式上架,核心 API 有兼容承诺 |
| 当前 | Kotlin 2.2 为稳定线(2025-06 GA),2.1.x 仍广泛在用 | 新项目直接用当前稳定线即可,具体小版本以官方发布页为准 |
一个常被问的问题:”iOS 端到底能不能用于生产?”以 2025 年年中的口径回答:能。KMP 逻辑层稳定已一年半,Compose Multiplatform 的 iOS 目标也在 2025-05 的 1.8.0 转入稳定,App Store 上架没有政策障碍。真正的分界线不在”能不能”,而在你团队有没有能力排查 iOS 侧的工具链问题——这需要至少一台 Mac。
工程结构:source set 怎么组织
KMP 工程按”源集(source set)”组织代码,commonMain 是共享层,各平台目录放平台专属实现。一个典型的三端工程长这样:
shared/
├── build.gradle.kts
└── src/
├── commonMain/kotlin/ # 共享层:数据模型、仓库、网络、业务规则
│ └── com/example/note/
│ ├── Note.kt # 纯 Kotlin 数据类,三端通用
│ ├── NoteRepository.kt # 依赖接口,不碰平台 API
│ └── Platform.kt # expect 声明(见下一节)
├── androidMain/kotlin/ # Android 实现 + Jetpack Compose UI 所在模块引用它
├── iosMain/kotlin/ # iOS 实现,编译为 iOS 框架供 SwiftUI 侧调用
└── desktopMain/kotlin/ # JVM 桌面实现(Windows/macOS/Linux)
commonMain 里只能依赖两种东西:纯 Kotlin,以及明确声明支持多平台的库。生态里常用的几块拼图:网络用 Ktor client,序列化用 kotlinx.serialization,并发用 kotlinx.coroutines,本地存储用 SQLDelight 或 Room(两者均支持多目标)。把一个 Android 专属依赖塞进 commonMain,编译器会直接报错而不是等到运行时——这是 source set 机制的价值:平台泄漏在编译期被拦下,而不是上线后崩给你看。
expect/actual:跨端的契约机制
共享代码总有绕不开平台差异的时刻:读个设备标识、发个本地通知、拿个主线程。KMP 的答案是 expect/actual——commonMain 里用 expect 声明”我承诺每个平台都有这个东西”,各平台目录用 actual 提供实现:
// commonMain/Platform.kt
// expect 只写契约不写实现:像接口,但由编译器逐目标检查
expect fun currentPlatform(): String
// commonMain/NoteRepository.kt
fun greeting(): String = "running on ${currentPlatform()}"
// androidMain/Platform.android.kt
actual fun currentPlatform(): String = "Android ${android.os.Build.VERSION.RELEASE}"
// iosMain/Platform.ios.kt
import platform.UIKit.UIDevice
actual fun currentPlatform(): String = "iOS ${UIDevice.currentDevice.systemVersion}"
// desktopMain/Platform.desktop.kt
actual fun currentPlatform(): String = "Desktop ${System.getProperty("os.name")}"
它和运行时反射方案的差别值得强调一遍:某个目标忘了写 actual,编译直接失败,错误信息会点名缺哪个目标的实现;不会出现”测试环境好好的,某个平台一跑就 NoSuchMethod”的运行时惊喜。代价是新增一个目标(比如从三端扩到 wasm)时,所有 expect 都要补对应 actual——契约是按目标数量倍增的,这决定了 expect 声明必须克制。
反模式点名:expect/actual 泛滥。一个健康工程里 expect 通常只有个位数到十几处,集中在平台能力薄薄一层;如果你发现 commonMain 里几十个 expect,说明平台差异没有被抽象掉,只是被切成碎片藏了起来,每加一端都要还一遍债。正确做法是把差异封装成小的平台服务接口,业务代码依赖接口而不是依赖 expect 函数。
跑起来:创建第一个工程
官方提供在线脚手架 Kotlin Multiplatform wizard(kmp.jetbrains.com),勾选目标平台与要集成的库(Networking 选 Ktor、Storage 选 Room 都可以),下载即得可运行的 Gradle 工程;Android Studio 与 JetBrains Fleet 也内置了同源向导。生成的工程直接承载上文的 source set 结构,不需要手工拼 Gradle 配置。命令不钉版本,脚手架产出的 Kotlin 版本以生成时官方模板为准,生成后第一件事是把实际版本记进项目文档。
三条来自常见踩坑的提示:
| 检查项 | 原因 |
|---|---|
| 勾了 iOS 目标,构建机必须是 macOS 且装好 Xcode | iOS 目标的编译与框架产物要走 Xcode 工具链,Linux/Windows 只能编非 Apple 目标 |
| 本地跑通 Android 与桌面目标再碰 iOS | 前两者不需要额外环境,用最低成本验证共享层逻辑 |
| 共享层先写纯逻辑(模型 + 仓库 + 一条 Ktor 请求) | 第一天就集成数据库与加密存储,排障时分不清是 KMP 的问题还是库的问题 |
入门阶段的另外两个坑
坑一:把”KMP 稳定”当成”我用的库都稳定”。稳定承诺作用于语言与工具链层面;具体某个多平台库的 API 成熟度要看它自己的文档(标注 Stable/Beta/Experimental)。尤其 composable UI 相关的实验 API,升级时行为变化不提前通知。给公共层选库时,把”是否标注 Stable”当硬条件。
坑二:一上来就追求”百分百共享”。共享率不是 KPI,百分之百共享通常意味着用最别扭的方式绕开平台差异。留出各端的 actual 层,让每端用它最顺手的系统 API,长期成本反而更低。
这篇教会了什么,下一步去哪
三句话收束:KMP 共享的是逻辑不是 UI,先统一逻辑层再看要不要全共享;expect/actual 是编译期契约,用得越少越健康;iOS 稳定性在 2025 年已不是 blocker,真正的门槛是团队的工具链排障能力。
下一步按顺序读:第一个 Compose 应用补 Android 侧 UI(Compose 语法在 Compose Multiplatform 里原样复用),协程与 Flow 基础补共享层里最常用的异步工具——协程与 Flow 在 commonMain 里一行不改地跨三端。
版本核实(2025-06 检索):Kotlin 当前稳定线 2.2(2025-06 GA,2.1.x 仍广泛在用);Kotlin Multiplatform 于 2023-11 随 Kotlin 1.9.20 宣布稳定;K2 编译器自 2024-05 的 Kotlin 2.0.0 起默认启用;Compose Multiplatform for iOS 自 2025-05 的 1.8.0 起稳定。以上均以 kotlinlang.org 官方发布页为准。