跨平台构建: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 跨平台构建流程

Kotlin Multiplatform 跨平台构建流程 commonMain 业务逻辑 + expect 声明 Gradle 编译:选择 actual 实现 JVM jar / aar Native klib / framework JS .js / .mjs Android / iOS / Web 应用集成

实际项目中的避坑指南

配置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的真正威力了。


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