这两年只要聊到跨平台开发,Kotlin Multiplatform(KMP)就是一个绕不开的话题。尤其是看到很多团队还在Android和iOS各维护一套业务逻辑,同样的网络请求写两遍、同样的数据校验写两遍、同样的本地存储写两遍,时间久了光想想都头大。KMP的核心价值正在于此:用一套Kotlin代码把业务逻辑、数据层、网络层全部沉淀下来,UI层各自用原生方案去写,既能享受多端复用的效率,又不必像Flutter那样完全放弃原生控件和平台能力。这篇文章面向的是正在评估KMP的移动端开发、以及已经决定上手但不知道从哪开始踩坑的Kotlin/Android开发者,我会从KMP的定位、工程搭建、核心机制、协程与Compose Multiplatform的实战,再到构建工具链里的疑难杂症,完整走一遍。
先说一个容易混淆的点:KMP里的KMP是Kotlin Multiplatform,跟字符串匹配算法里的KMP(Knuth-Morris-Pratt,next数组那个)没有任何关系。网上搜KMP教程经常混进一大堆算法题解,如果你是想学跨平台,千万别看岔了。我下面讲的KMP,全称Kotlin Multiplatform,是JetBrains和Google持续投入的跨平台方案,直白点说就是用Kotlin写共享代码,编译成Android的字节码和iOS的二进制产物。
1. 别再叫它跨端框架了,KMP的本质是“共享代码层”
很多人在选型的时候会把KMP和Flutter、React Native放在一个维度里去比,这其实是个误区。Flutter和RN是完整的UI跨端方案,从渲染层到组件库到状态管理全给你包圆了;而KMP默认不管UI,它的职责是让你把“不依赖平台”的代码抽出来共享,UI怎么画、用Jetpack Compose还是SwiftUI,那是你自己的事。
1.1 一套代码,到底共享了什么
纯业务逻辑是KMP共享收益最高的部分。包括但不限于:
- 数据模型定义:比如用户对象、订单状态、接口请求和响应体,这些字段映射、序列化逻辑写一遍就够了。
- 网络请求层:用Ktor Client搭配Kotlinx Serialization,一套代码同时跑在OkHttp和Darwin引擎上。
- 本地存储:用SQLDelight做多端数据库,或者用Multiplatform Settings封装SharedPreferences和NSUserDefaults。
- 数据校验与格式化:正则匹配、表单校验、日期处理、数值计算,这些不依赖平台API的逻辑在KMP里能原样复用。
这样拆分下来你会发现,一个App里大概有40%到60%的代码是纯粹的“业务逻辑和数据处理”,这些恰恰是KMP能吃下的部分。而UI层、系统能力调用、埋点SDK这类强依赖平台SDK的东西,仍然留在各端的工程里。
1.2 为什么不是Flutter,也不是RN
Flutter用Dart,RN用JavaScript,这两者本质上都是“自绘UI + 平台通道”的结构。好处是UI一致性高、开发效率高,但代价是性能损耗、平台能力访问不直接、以及一套独立生态。KMP走了另一条路:不动你的UI,只共享逻辑。这对已有原生工程、不太想推翻重来的团队特别友好——你把基础业务包抽到shared模块,Android工程和iOS工程各自依赖这一份代码,渐进式改造。
另外值得一提,KMP对已有Kotlin Android工程师几乎是零学习成本的。IntelliJ IDEA和Android Studio官方支持KMP工程,很多现有的Java/Kotlin代码经过简单调整就能放进shared模块。而JetBrains这些年还在推Compose Multiplatform(后文简称Compose MP),它是基于Kotlin和Jetpack Compose实现的跨平台UI方案,目前iOS支持已经进入稳定阶段。如果你愿意把UI也交给Compose写,那KMP就能进化成真正意义上的“逻辑+UI双共享”方案。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 动手搭第一个KMP工程:从零到三端跑通
搭KMP工程的方式很多,我觉得对新手最友好的还是直接用JetBrains官方的Kotlin Multiplatform Wizard生成模板,它避免了手工配置Gradle时各种版本对齐的麻烦。你可以打开KMP模板站点(kotlinlang.org/multiplatform-wizard),选择Android + iOS,框架那一栏选Compose Multiplatform,风格选Empty Compose Multiplatform App,然后Generate得到一个zip包。这个模板已经包含了shared模块、androidApp模块和iosApp模块,以及composeApp的UI层骨架。
2.1 用模板工程跑起来
解压后用Android Studio打开,首次同步Gradle会下载一堆依赖,耐心等就行。模板工程里的目录结构大致是这样的:
code复制KmpProject/
├── composeApp/ # 放Compose MP共享UI和业务逻辑
│ └── src/
│ ├── commonMain/ # 多端共享代码
│ ├── androidMain/ # Android平台实现
│ └── iosMain/ # iOS平台实现
├── iosApp/ # Xcode工程,用于iOS入口
├── gradle/
└── build.gradle.kts
这里要着重说明一下:如果你用过早期的KMP模板,可能会记得shared模块和androidApp/iOSApp分离的模式。现在官方推荐的KMP模板里增加了composeApp模块,用于承载共享的Compose UI。commonMain是真正的金矿,底下放的所有代码都可以同时编译到Android和iOS。androidMain和iosMain则用来放expect/actual对应的平台实现,后面会细说。
2.2 Android端跑法
直接运行androidApp模块,或者在某些版本模板里直接run composeApp的android目标就行。如果你是Android开发者,这一步基本零难度,构建产物就是一个普通的APK,Kotlin代码被编译成了Dalvik字节码。
2.3 iOS端跑法
iOS那边比较复杂。模板里的iosApp是一个Xcode工程,里面通过一个脚本调用Gradle任务嵌入Kotlin框架。在macOS上用Android Studio打开iosApp目录下的.xcodeproj,用Xcode直接跑。比较关键的一点是,KMP生成的iOS框架有几种二进制格式,模板默认用的是debug静态框架,你在真机上调试时可能需要在Xcode里改一下Build Settings里的其他链接器标志(Other Linker Flags),把模板自带的那一堆-framework参数删掉,后续版本模板已经处理了这个问题,但老版本还是要手动留意。
跑通模板工程本身不是目的,目的是确认你本地的JDK版本、Android SDK、Xcode版本、Gradle版本、Kotlin版本互相兼容。KMP对版本敏感度比普通Android项目高出不少,我见过太多case都是卡在这一步。
3. 共享代码的核心机制:expect/actual和模块化设计
模板跑通之后,真正动手写业务代码前,必须先吃透KMP最核心的语法机制——expect/actual。这是KMP用来处理“同一逻辑,平台不同实现”的官方方案。
3.1 expect/actual是怎么工作的
你在commonMain里声明一个expect关键字修饰的函数或类,意思是“我要求所有平台都提供一个名为xxx的实现”。然后在androidMain和iosMain里各写一个actual实现,编译的时候Kotlin编译器会把commonMain和当前平台的actual对接起来。
举个例子,一个获取当前平台语言环境的工具类:
kotlin复制// commonMain/Platform.kt
expect fun getPlatformLanguage(): String
// androidMain/Platform.android.kt
actual fun getPlatformLanguage(): String {
return java.util.Locale.getDefault().language
}
// iosMain/Platform.ios.kt
actual fun getPlatformLanguage(): String {
return NSLocale.currentLocale.languageCode ?: "en"
}
这样上层逻辑调用getPlatformLanguage()时,就不用关心实际是跑在哪个系统上。expect/actual支持函数、类、属性、注解等多个维度,实际项目中常用它来做日期获取、设备信息、日志上报、文件目录获取这些平台差异点。
3.2 不是所有东西都要塞进commonMain
这是新手最容易走偏的地方。KMP虽然能跨平台,但不是说所有代码都放commonMain就好。commonsMain的代码有个硬性限制:只能调用Kotlin标准库、Kotlinx库以及各平台都支持的第三方库。任何依赖Android SDK或iOS SDK的API,都必须通过expect/actual隔离出去。
我通常会把shared模块拆成几个层:
- 数据中心:定义数据模型,用Ktor Client做网络请求,用SQLDelight或Multiplatform Settings做存储。
- 业务中心:处理交互逻辑、状态管理,这一层不关心数据从哪来,只调用数据中心接口。
- 平台能力适配层:凡是涉及文件系统、设备信息、定位等系统能力,统一在这个层做expect/actual。
这样拆的好处是,后续如果某天不是KMP了,把对应模块单独拎出来也能继续服务于单一的Android或iOS工程,不至于代码和数据源粘死在一起。
3.3 网络层落地示例
用Ktor Client写一套同时支持Android和iOS的网络层,是KMP入门最经典的练手项目。模板里如果没有预置依赖,你需要在commonMain的build.gradle.kts里加上:
kotlin复制sourceSets {
commonMain.dependencies {
implementation("io.ktor:ktor-client-core:2.3.8")
implementation("io.ktor:ktor-client-content-negotiation:2.3.8")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.8")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
}
androidMain.dependencies {
implementation("io.ktor:ktor-client-okhttp:2.3.8")
}
iosMain.dependencies {
implementation("io.ktor:ktor-client-darwin:2.3.8")
}
}
这样一套依赖写下来,commonMain里就能放心用统一的HttpClient了。Android和iOS各自使用自己平台的网络引擎,对外暴露的接口完全一致。
4. 协程与Compose Multiplatform:UI层也能共享吗
KMP和Kotlin协程是一对天然组合,协程本身是Kotlin标准库的一部分,天然具备跨平台能力。你在commonMain里写viewModelScope.launch {}、flow.collect {},在iOS端同样能跑。这一点很重要,因为很多业务异步逻辑如果各端分开写,光是对齐回调时序就能逼疯人。
4.1 协程跨平台,确实不是吹的
官方的kotlinx.coroutines为Android提供了Dispatchers.Main,iOS也有Main dispatcher的实现,所以你在共享代码里可以放心用Dispatchers.Main切回主线程。模板工程里用Compose MP写UI的时候,状态管理和协程作用域可以完全放在commonMain里。
举个例子,一个简单的倒计时逻辑:
kotlin复制class CountdownViewModel : ViewModel() {
private val _remainingSeconds = MutableStateFlow(60)
val remainingSeconds: StateFlow<Int> = _remainingSeconds.asStateFlow()
fun start() {
viewModelScope.launch {
while (_remainingSeconds.value > 0) {
delay(1000)
_remainingSeconds.value -= 1
}
}
}
}
这段代码放在commonMain里,Android端能用,iOS端同样能用,UI层只需要collect这个StateFlow并刷新界面。这就是协程跨平台最直观的收益。
4.2 Compose MP:是UI的未来还是“又一个Flutter”
刚才我提过Compose Multiplatform。它跟Jetpack Compose的API几乎一致,底层是Skia渲染引擎,这意味着你用Compose语法写出来的UI,编译后既能在Android上运行,也能在iOS上运行。目前Compose MP的iOS支持已经相对成熟,很多团队已经拿它做内部工具类App的跨端UI了。
Compose MP的优点是代码复用率极高,一套Compose代码两端跑,动画和布局逻辑完全一致,完全不用考虑Android XML和iOS Auto Layout两套体系。缺点嘛,也很明显,上手成本比原生UI要高一点,而且如果涉及到复杂的手势冲突、平台控件差异,依然要写expect/actual去做适配。
有人会问:“那既然Compose MP这么强,我直接用KMP + Compose MP全跨端,还要SwiftUI干嘛?”这个问题没有标准答案。如果你的App偏内容展示、业务逻辑重、界面不太复杂,Compose MP能显著提升交付速度。但如果是想追求极致流畅、深度依赖iOS原生交互体验,那还是老老实实保留SwiftUI,UI层各自写,只共享逻辑层。
4.3 实时热力图的KMP实战思路
网上有个挺热的搜索词是“kotlin安卓maplibre做一个实时显示热力图”,这个场景放在KMP下其实很有代表性。地图SDK本身,MapLibre是支持Android和iOS的,但热力图的数据处理、点位聚合、时间窗口计算这些逻辑完全可以在commonMain里复用。
大致的模块划分是:commonMain里写一个HeatmapDataProcessor,负责接收原始GPS点位、按时间切片、计算网格密度;Android端用MapLibre Android SDK渲染热力层;iOS端用MapLibre Native iOS渲染。这样多端地图SDK不一样,但热力算法同一套,效果和性能却不输任何原生实现。
5. 构建工具链里最折磨人的三个坑:版本、版本、还是版本
KMP开发到我心里的一个主旋律就是“版本对齐”。跟纯Android工程相比,KMP多出了iOS Targets、CocoaPods、Xcode构建集成等维度,任何一个环节的版本错位,都可能引发莫名其妙的构建失败。
5.1 “module was compiled with an incompatible version of Kotlin”——这个报错几乎必遇
搜索热词里有这么一条:error: kotlin: module was compiled with an incompatible version of kotlin. th。这个报错在KMP开发里非常常见,基本根因只有一个——本地Gradle插件版本和依赖库编译时用的Kotlin版本不一致。
打个比方,你项目里的Kotlin是1.9.22,但某个第三方库是用Kotlin 2.0.20编译发布的,那你在构建时就会看到这个不兼容错误。多见于Ktor、SQLDelight、Compose插件等频繁更新编译器的库。
解决办法有两个方向:
- 优先升级项目Kotlin版本,让项目Kotlin版本 >= 依赖库的编译版本,这个最省事。
- 如果某个库必须用旧版本,就得去查它对应的Kotlin版本区间,把项目版本对齐。
建议在根目录的gradle/libs.versions.toml里把Kotlin版本、Compose版本、Ktor版本、协程版本全部集中声明,方便统一管理。千万别在多个build.gradle.kts里各写各的版本号,坑死人不偿命。
5.2 Gradle版本不兼容Kotlin的问题
另一个高频问题:gradle-5.6.4-all不支持kotlin吗。没错,Gradle 5.6.4这个版本对于新版Kotlin插件来说太旧了。Kotlin 1.8+一般要求Gradle 7.3以上,Kotlin 2.0+则基本要求Gradle 8.0以上。如果你还在用老的Gradle版本,要么升级Gradle,要么降级Kotlin插件,没有第三条路。
判断标准很简单:Android Studio里打开File -> Project Structure -> Project,看Android Gradle Plugin版本和Kotlin版本,再查一下官方兼容表。我个人的稳妥组合是Kotlin 2.0.x + Gradle 8.5+ + AGP 8.2+,这个组合在KMP和Compose MP上跑得比较省心。
5.3 Xcode与Kotlin Framework的版本博弈
iOS端的构建还多一层Xcode版本的变量。KMP生成的Framework要求Xcode版本不能太旧,否则链接阶段会报一些匪夷所思的错误,比如找不到某些符号。
一个常见的坑是,在Apple Silicon芯片和Intel芯片之间切换构建目标时,Kotlin/Native的编译缓存需要清理。我遇到过几次这种情况:真机编译报某条链接错误,翻来覆去查不出原因,最后用./gradlew clean清一遍缓存就好了。别嫌粗暴,KMP工程里clean是解决很多玄学问题的万能钥匙。
6. 什么时候该用KMP,什么时候别硬上
最后我想聊聊KMP的适用边界。这东西确实好,但并不是所有项目都适合用它。
先说什么项目适合KMP:
- 你的Android和iOS团队已经存在,但不想各自维护一整套业务逻辑。
- 产品逻辑复杂,状态多,数据流一致,比如电商结算、投资理财、表单流程。
- 团队有Kotlin基础,愿意投入学习KMP和编译链路的成本。
- 目标是渐进式改造,先把数据层和业务层抽到共享模块,UI层仍然原生。
什么项目不建议硬上KMP:
- 项目以UI效果和交互体验为核心竞争力,两端视觉差异大,逻辑复用少。
- 团队没有Kotlin开发经验,全部是iOS Swift开发,学习曲线会比较陡。
- 项目已经用了很多成熟但只支持单一平台的第三方SDK,比如某些登录SDK、支付SDK、推送SDK,多端适配的工作量可能超过复用逻辑省下来的时间。
我在实际项目里的体会是:KMP最适合从“中间层”开始切入,不要一上来就想把整个世界都塞进commonMain。你先让网络请求和序列化共享起来,再让数据校验和状态管理共享起来,逐步扩展边界。这种渐进式打法既稳又能及时看到收益,比大刀阔斧重构保险得多。
最后分享一个开发习惯上的小建议:KMP工程里日志打印这个看似简单的需求,其实也建议用expect/actual封装一层工具类,让Android输出到Logcat,iOS输出到统一日志体系中,并把类名、行号、时间戳都带进去。排查问题的时候这种细节能省很多时间。KMP的魅力就在于此——它不强迫你全盘跨端,只需要在合适的地方跨界,就能把团队从重复劳动的泥潭里拉出大半截。至于UI层要不要用Compose MP一起跨,那又是另一个可以认真算账的话题了。
