KMP这名字,我在后台收到过无数次私信,问得最多的就是“它跟Flutter比到底强在哪”以及“我Android开发两年,学这个划算吗”。说实话,Kotlin Multiplatform(以下简称KMP)并不是什么新鲜概念,但它在2024到2025年这个节点上,确实到了值得投入精力去啃的阶段。原因很简单:生态成熟了、坑填平了、大厂背书也够多了,更重要的是,它解决的不只是“跨平台”这个表层的需求,而是“跨平台之后,业务逻辑和团队心智怎么统一”这个更深层的痛点。
这篇文章我不打算泛泛地讲“KMP能做什么”,那没意思。我更想跟你掰扯清楚的是,KMP这套东西从原理上到底是怎么运转的,以及在你真正动手写一个跨平台模块时,那些文档上没写清楚、但又绝对会踩到的现实问题。全文会涉及我从工程搭建到协程架构、再到各种编译报错排查的真实经历,内容偏长,但看完你应该能对KMP有一个从“听说过”到“能上手”的实质性认知。
1. 为什么是KMP:一套代码背后的博弈逻辑
在动手写第一行expect声明之前,你得先想明白一个根本问题——你为什么要让Kotlin代码在iOS上跑起来?这个问题的答案直接决定了你的架构设计和代码分层方式。
1.1 跨平台方案的取舍:UI层与逻辑层的分离
很多人一谈跨平台,第一时间想到的就是Flutter或者React Native,因为它们能做到“一套UI代码两端运行”。而KMP从一开始走的就是一条完全不同的路:它只共享逻辑层,不共享UI层。Android端你可以继续用Jetpack Compose,iOS端你可以继续用SwiftUI,二者互不干扰,但业务逻辑、数据仓库、网络请求、持久化缓存这些“硬核”部分,则通过KMP在Kotlin层统一实现。
KMP这个策略背后的逻辑其实特别务实:UI层是变化最频繁、与平台系统特性耦合最深的部分。Android的返回手势、iOS的侧滑返回、Android的曲面屏适配、iOS的灵动岛交互,这些细节天然就应该由原生代码来写。而真正消耗开发精力的,往往是那套“一万个页面都要用”的业务状态管理和数据流转逻辑。把这两者拆开,让最稳定的部分跨平台复用,让最灵活的部分留在各端手写,这就是KMP的核心博弈逻辑。
这套设计的直接好处是:改动一个下单逻辑,不再需要Android一个人、iOS一个人各写一遍,然后还要一帧一帧地对需求。代码共享之后,逻辑一致性是天然保证的,而不是靠两个同事的沟通来保证的。我自己体验下来,团队协调成本下降的幅度比代码编写成本下降的幅度还大。
1.2 KMP不是Android的附庸:它的定位是独立技术栈
还有一个很重要的认知需要纠正:KMP并不是“Android技术的延伸”,而是一套独立的、跨平台的技术栈。也就是说,你哪怕没有深厚的Android开发背景,只要Kotlin语言基础扎实,照样可以用KMP写出不错的共享模块。反过来,Android开发者用KMP时,也需要注意不能把iOS平台当成“二等公民”。
KMP在Gradle里配置的target,既包括androidTarget(),也包括iosArm64()、iosSimulatorArm64()、iosX64(),这些iOS target跟Android target是平级的。你在代码里写的expect声明,必须在iOS的actual实现里给出完整的、符合iOS平台规范的实现,不能因为Android是默认平台就优先实现Android的部分然后iOS随便糊弄。简单来说,KMP把Android和iOS放在了同一个天平上,这要求你用更中立的视角去看待两端的能力差异。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 从零搭建第一个KMP工程:Gradle里藏着的门道
搭建KMP工程,第一关就是Gradle配置。这地方的门道特别多,很多人一上来就卡在SDK版本、插件版本和Kotlin版本的兼容性问题上。我记得有段时间,只要Kotlin版本跟Gradle版本稍微不对付,编译时就冒出各种莫名其妙的报错,比如热搜词里那条“module was compiled with an incompatible version of Kotlin”,就是典型的版本错乱导致的。
2.1 核心构建脚本的精读:每个配置项都在干什么
一个标准的KMP模块,build.gradle.kts大概长这样:
kotlin复制plugins {
kotlin("multiplatform") version "2.0.20"
kotlin("plugin.serialization") version "2.0.20"
}
kotlin {
androidTarget {
compilerOptions {
jvmTarget.set(JvmTarget.JVM_17)
}
}
listOf(
iosArm64(),
iosSimulatorArm64(),
iosX64()
).forEach { iosTarget ->
iosTarget.binaries.framework {
baseName = "SharedKit"
isStatic = true
}
}
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.1")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.2")
implementation("io.ktor:ktor-client-core:3.0.0")
implementation("io.ktor:ktor-client-content-negotiation:3.0.0")
implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.0")
}
}
val androidMain by getting {
dependencies {
implementation("io.ktor:ktor-client-okhttp:3.0.0")
}
}
val iosMain by getting {
dependencies {
implementation("io.ktor:ktor-client-darwin:3.0.0")
}
}
}
}
android {
namespace = "com.example.shared"
compileSdk = 35
}
这里我想重点说三个关键点。
第一,iosArm64()、iosSimulatorArm64()和iosX64()这三个target必须同时配置。真机是arm64架构,模拟器在Apple Silicon上是arm64模拟器架构,在Intel Mac上是x64架构。缺了任何一个,你都会在某种运行环境下拿不到framework包。我第一次搭工程的时候偷懒只配了iosArm64(),结果在模拟器上跑的时候死活引用不到共享模块,折腾了大半夜才反应过来是target没配全。
第二,isStatic = true这个参数。它决定生成的framework是静态库还是动态库。我强烈建议用静态库。理由是动态库在App启动时需要做动态链接,会增加启动耗时,而且偶尔会跟其他依赖的静态库产生符号冲突,排查起来特别痛苦。静态库则完全没有这个问题,代价只是编译时链接稍微慢一点,但换来的是运行时稳定。
第三,sourceSets的依赖声明。你在commonMain里写的依赖,是两端共享的。但网络引擎的实现细节在Android和iOS上完全不同——Android上通常用OkHttp,iOS上则需要用Darwin引擎(底层封装了NSURLSession)。所以ktor-client-core放在commonMain,而ktor-client-okhttp和ktor-client-darwin分别放在androidMain和iosMain里。KMP的sourceSets机制会自动帮你做这个“平台相关的依赖注入”,这也是它比Java或者传统JVM技术栈优雅得多的地方。
2.2 与现有Android工程的融合:构建产物怎么被消费
共享模块写好之后,Androi
