跨平台构建:JVM、Native、JS项目配置、Kotlin Multiplatform
说实话,我第一次接触跨平台构建时,心里是有点发怵的。你想啊,一套代码要跑在JVM、Native、JS三个平台上,每个平台的编译方式、依赖管理、运行时环境都不一样,这配置起来得多复杂?
但Gradle Kotlin DSL把这层窗户纸捅破了。它用类型安全的方式,把跨平台构建的复杂度封装得明明白白。今天我就带你走一遍,看看Kotlin Multiplatform项目到底怎么配。
Kotlin Multiplatform 的核心思想
KMP(Kotlin Multiplatform)说白了就是「一次编写,到处运行」的现代版。但它不是简单的代码复制,而是通过期望-实际(expect/actual)机制,让你在共享业务逻辑的同时,又能调用各平台的原生API。
核心原则:共享代码放在 commonMain 里,平台相关代码放在 jvmMain、nativeMain、jsMain 里。Gradle 负责把这些模块拼装成最终产物。
我在项目中遇到过最典型的场景:一个网络请求库,业务逻辑完全一样,但底层HTTP客户端在Android上用OkHttp,在iOS上用NSURLSession。这时候KMP的架构就派上用场了。
项目结构:从零搭建KMP项目
我们先看一个标准的KMP项目长什么样。嗯,这里要注意,目录结构是有约定的,别自己瞎起名字。
// 项目根目录
my-kmp-app/
├── build.gradle.kts
├── settings.gradle.kts
├── shared/
│ ├── build.gradle.kts
│ └── src/
│ ├── commonMain/ // 共享代码
│ ├── jvmMain/ // JVM 平台实现
│ ├── nativeMain/ // Native 平台实现
│ └── jsMain/ // JS 平台实现
├── androidApp/
│ └── build.gradle.kts
└── iosApp/
└── build.gradle.kts
你想想看,这种结构天然就把关注点分开了。commonMain 里写纯Kotlin代码,不依赖任何平台库。其他三个目录各自实现平台相关的接口。
build.gradle.kts 配置详解
配置KMP项目,核心就是 kotlin {} 块里的声明。我习惯先声明目标平台,再配置源码集依赖。
// shared/build.gradle.kts
plugins {
kotlin("multiplatform") version "1.9.22"
id("maven-publish")
}
kotlin {
// 声明目标平台
jvm {
compilations.all {
kotlinOptions.jvmTarget = "17"
}
}
js(IR) {
browser()
nodejs()
}
linuxX64() // Native 目标
macosX64()
iosX64()
iosArm64()
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.0")
}
}
val commonTest by getting {
dependencies {
implementation(kotlin("test"))
}
}
val jvmMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
}
}
val jsMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-js:1.7.3")
}
}
val nativeMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core-native:1.7.3")
}
}
}
}
我的经验:依赖版本一定要统一。我曾经因为 commonMain 和 jvmMain 用了不同版本的协程库,导致运行时出现奇怪的 ClassCastException。后来我统一用 kotlinx-coroutines-core 的 BOM 来管理版本,再也没出过问题。
expect/actual 机制:跨平台的核心
这是KMP最巧妙的设计。你在 commonMain 里声明一个「期望」,然后在各平台里提供「实际」实现。
// commonMain/kotlin/com/example/Platform.kt
expect fun getPlatformName(): String
// jvmMain/kotlin/com/example/Platform.kt
actual fun getPlatformName(): String = "JVM ${System.getProperty("java.version")}"
// nativeMain/kotlin/com/example/Platform.kt
actual fun getPlatformName(): String = "Native (${Platform.memoryModel})"
// jsMain/kotlin/com/example/Platform.kt
actual fun getPlatformName(): String = "JS (${js("navigator.userAgent")})"
你看,commonMain 里只写接口签名,具体实现交给各平台。Gradle 在编译时会自动选择正确的 actual 实现。说白了,这就是编译期的多态。
构建产物配置
不同平台需要不同的产物格式。JVM 要 jar,Native 要动态库或静态库,JS 要 bundle。这些都可以在 kotlin {} 块里配置。
| 平台 | 产物类型 | 配置方式 |
|---|---|---|
| JVM | jar / aar | jvm { withJava() } |
| Native | .klib / .framework | iosX64 { binaries.framework { ... } } |
| JS | .js / .mjs | js(IR) { browser { ... } } |
// 配置 iOS Framework 输出
kotlin {
iosX64()
iosArm64()
iosSimulatorArm64()
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
it.binaries.framework {
baseName = "shared"
isStatic = true
}
}
}
我曾经踩过的坑:Native 平台的 Framework 名称不能有特殊字符,否则 Xcode 集成时会报错。还有,如果你同时配置了多个 iOS 架构,记得用 listOf() 统一处理,不然每个架构都要写一遍配置,代码冗余不说,还容易漏掉。
SVG:KMP 跨平台构建流程
实际项目中的避坑指南
配置KMP项目时,有几个地方特别容易出问题。我一个个说。
- 依赖冲突:不同平台的同一个库可能有不同 artifact ID。比如
kotlinx-coroutines-core在 JVM 上就是这个名字,但在 Native 上要加-native后缀。我建议用implementation(platform("org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.7.3"))统一管理。 - 序列化配置:如果你用
kotlinx-serialization,记得在每个平台的 sourceSet 里都加上withSerialization插件,不然编译会报错。 - iOS Framework 集成:Xcode 项目引用 KMP 生成的 Framework 时,需要配置
Build Phases里的脚本。我习惯用 Gradle 的embedAndSignAppleFrameworkForXcode任务,省心。
我的小技巧:在 gradle.properties 里加上 kotlin.mpp.stability.nowarn=true,可以屏蔽KMP实验性API的警告。虽然不推荐长期使用,但在快速原型阶段能省不少事。
总结一下
KMP的跨平台构建,核心就是三件事:声明目标平台、配置源码集依赖、实现 expect/actual。Gradle Kotlin DSL 的类型安全特性,让这些配置变得可追溯、可重构。你想想看,以前用 Groovy 写跨平台配置,一个拼写错误要等到运行时才能发现,现在编译期就给你报出来了。
我个人习惯在项目初期就把所有目标平台都声明好,哪怕暂时只开发 JVM 版本。因为后续加平台时,Gradle 的配置变更会影响整个构建缓存,不如一开始就规划好。
好了,跨平台构建这块就聊到这儿。记住,配置只是手段,核心还是你的业务代码怎么在三个平台上优雅地复用。多写几个 expect/actual 接口,你就能体会到KMP的真正威力了。