29、Gradle与Kotlin多平台:Kotlin Multiplatform插件、源码集配置、平台目标配置

Kotlin Multiplatform(简称 KMP)是 JetBrains 推出的跨平台方案。它允许你用一套 Kotlin 代码,同时编译到 JVM、Android、iOS、JS、Native 等多个平台。说白了,就是「一次编写,到处运行」的 Kotlin 版本。

我最早接触 KMP 是在一个共享业务逻辑的项目里。当时团队既要写 Android 又要写 iOS,两边的数据模型和网络层几乎一模一样。我们试着用 KMP 把公共代码抽出来,效果出奇的好。嗯,这里要注意,KMP 不是要取代原生 UI,它主要负责业务逻辑层和数据层的复用。

Kotlin Multiplatform 插件

要在 Gradle 里启用 KMP,第一步就是应用插件。核心插件只有一个:org.jetbrains.kotlin.multiplatform

plugins {
    kotlin("multiplatform") version "1.9.22"
}

这个插件会帮你做几件事:

  • 注册 KMP 相关的 DSL 扩展(比如 kotlin {} 块)
  • 自动配置源码集(source set)的依赖关系
  • 根据你声明的平台目标,生成对应的编译任务

我个人习惯在 settings.gradle.kts 里统一管理插件版本,这样多模块项目不会乱。

小提示:KMP 插件对 Gradle 版本有要求。我建议用 Gradle 7.6 以上,否则可能会遇到一些奇怪的兼容性问题。

平台目标配置

平台目标(target)就是你要编译到哪些平台。每个目标对应一个编译产物,比如 JVM 的 jar、iOS 的 framework、JS 的 bundle。

配置方式很简单,在 kotlin {} 块里声明即可:

kotlin {
    jvm()                     // JVM 目标
    js(IR) {                  // JS 目标,使用 IR 编译器
        browser()
        nodejs()
    }
    iosX64()                  // iOS 模拟器(x86)
    iosArm64()                // iOS 真机(ARM)
    iosSimulatorArm64()       // iOS 模拟器(Apple Silicon)
    linuxX64()                // Linux 桌面
    macosX64()                // macOS 桌面
    mingwX64()                // Windows 桌面
}

每个目标还可以配置额外的参数。比如 JS 目标可以指定模块类型:

js(IR) {
    moduleName = "my-lib"
    binaries.executable()
    browser {
        webpackTask {
            outputFileName = "my-lib.js"
        }
    }
}

我曾经在配置 iOS 目标时踩过一个坑:如果你同时声明了 iosX64iosArm64,但忘了加 iosSimulatorArm64,那在 Apple Silicon 的 Mac 上跑模拟器就会报错。嗯,现在新版的 KMP 插件已经默认包含这三个了,但旧版本需要手动加。

源码集配置

源码集(source set)是 KMP 的核心概念。它把代码按「共享范围」分成几层:

  • commonMain / commonTest:所有平台共享的代码
  • jvmMain / jvmTest:仅 JVM 平台使用的代码
  • jsMain / jsTest:仅 JS 平台使用的代码
  • iosMain / iosTest:所有 iOS 变体共享的代码
  • iosX64Main / iosX64Test:仅某个具体 iOS 变体使用的代码

默认情况下,KMP 插件会自动生成这些源码集。你只需要在 src/ 目录下创建对应的文件夹即可:

src/
├── commonMain/
│   └── kotlin/
│       └── com/example/
│           └── Platform.kt
├── jvmMain/
│   └── kotlin/
│       └── com/example/
│           └── Platform.jvm.kt
├── iosMain/
│   └── kotlin/
│       └── com/example/
│           └── Platform.ios.kt
└── jsMain/
    └── kotlin/
        └── com/example/
            └── Platform.js.kt

你想想看,这种结构天然就支持「共享逻辑 + 平台适配」的模式。你在 commonMain 里写接口,在 jvmMainiosMain 里分别提供实现。

核心原则:commonMain 里只能使用 Kotlin 标准库和跨平台库(比如 kotlinx.coroutines、kotlinx.serialization)。平台相关的 API(比如 Java 的 File、iOS 的 Foundation)只能在对应的平台源码集里使用。

依赖管理

KMP 的依赖配置也分平台:

kotlin {
    sourceSets {
        commonMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
        }
        jvmMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3")
        }
        iosMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
        }
    }
}

注意,kotlinx-coroutines-core 在 commonMain 和 iosMain 里都出现了。这是因为 iOS 目标没有独立的协程库,直接用 core 就行。而 JVM 有专门的 JDK 8 适配版本。

我记得有一次,团队里有人把 kotlinx-serialization-json 只加在了 jvmMain 里,结果 iOS 编译时找不到 JSON 解析器。排查了半天才发现是依赖范围配错了。所以我的建议是:能用 commonMain 的依赖,尽量放在 commonMain 里

知识体系结构图

Kotlin Multiplatform 知识体系 KMP 插件 平台目标配置 源码集配置 依赖管理 jvm() / js() / iosX64() commonMain / jvmMain / iosMain implementation / api / compileOnly 一套代码,多平台编译,共享业务逻辑 平台差异通过源码集隔离

实际项目中的配置模板

下面是一个比较完整的 build.gradle.kts 示例,涵盖了常见的平台目标和源码集配置:

plugins {
    kotlin("multiplatform") version "1.9.22"
    id("com.android.library") version "8.1.0"
    kotlin("plugin.serialization") version "1.9.22"
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "17"
            }
        }
    }
    
    jvm()
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
    }
    
    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.2")
            }
        }
        
        val androidMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")
            }
        }
        
        val jvmMain by getting {
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-jdk8:1.7.3")
            }
        }
        
        val iosX64Main by getting
        val iosArm64Main by getting
        val iosSimulatorArm64Main by getting
        val iosMain by creating {
            dependsOn(commonMain)
            iosX64Main.dependsOn(this)
            iosArm64Main.dependsOn(this)
            iosSimulatorArm64Main.dependsOn(this)
            dependencies {
                implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
            }
        }
    }
}
注意:如果你同时配置了 Android 和 JVM 目标,记得在 androidMain 里不要使用 JVM 专属的 API(比如 java.io)。Android 虽然也是 JVM 系,但 API 并不完全一致。我曾经因为用了 java.nio.file.Path 导致 Android 编译失败,后来改成了 java.io.File 才解决。

常见问题与避坑

  • iOS 框架打包:如果你要生成 iOS framework,记得在目标配置里加上 binaries.framework。否则 Xcode 集成时会找不到产物。
  • 源码集继承关系iosMain 不会自动继承 commonMain 的依赖。你需要手动调用 dependsOn(commonMain)。插件默认只帮你建了文件夹,但依赖链要自己配。
  • Android 与 JVM 共存:如果同时声明了 androidTargetjvm(),注意 androidMainjvmMain 是独立的。不要指望 Android 能自动复用 JVM 的代码。
  • Ktor 等三方库:很多跨平台库已经提供了平台适配。比如 Ktor 的 ktor-client-core 放在 commonMain,ktor-client-okhttp 放在 jvmMain,ktor-client-darwin 放在 iosMain。照着官方文档配就行。

嗯,KMP 的配置其实不复杂,核心就是理解「平台目标」和「源码集」这两个概念。你只要记住:commonMain 写逻辑,平台源码集写适配,大部分场景都能搞定。


公众号:蓝海资料掘金营,微信deep3321