21、Compose与Kotlin Multiplatform:Compose编译器配置、KMP模块配置、共享代码、平台特定依赖、发布KMP库

聊到Kotlin Multiplatform(简称KMP),我其实挺感慨的。早些年做跨平台,要么用Flutter,要么上RN,Kotlin这边一直差点意思。直到JetBrains把Compose Multiplatform推出来,我才觉得「嗯,这才是Kotlin该有的样子」。说白了,KMP让你用一套代码写UI逻辑,同时还能调用各平台的原生API。今天我们就把它拆开揉碎了讲清楚。

KMP + Compose 模块 Compose编译器配置 KMP模块配置 共享代码 平台特定依赖 发布KMP库 expect/actual 一套代码 → Android/iOS/Desktop 三端复用

Compose编译器配置:别小看这几行

Compose之所以能实现声明式UI,靠的是编译器插件。它会在编译期把带@Composable注解的函数做代码变换。你想想看,如果没有这个编译器插件,Compose根本跑不起来。

在KMP项目里配置Compose编译器,我建议直接在根项目的build.gradle.kts里声明版本。这样所有子模块都能统一,避免版本冲突。

// 根项目 build.gradle.kts
plugins {
    id("org.jetbrains.kotlin.multiplatform") version "1.9.22" apply false
    id("org.jetbrains.compose") version "1.6.0" apply false
}

然后在具体的KMP模块里这样配:

// shared/build.gradle.kts
plugins {
    kotlin("multiplatform")
    id("org.jetbrains.compose")
}

kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material3)
            }
        }
    }
}
⚠️ 注意: Compose编译器版本必须与Kotlin版本严格对应。我曾经因为升级Kotlin忘了同步升级Compose,结果编译报了一堆「Unsupported Kotlin version」的错误。查了半天才发现是版本不匹配。

KMP模块配置:sourceSet是核心

KMP的模块配置,说白了就是告诉Gradle:「哪些代码是共用的,哪些代码是各平台独有的」。这个通过sourceSets来实现。

我个人习惯把模块结构分成三层:

  • commonMain — 放共享的业务逻辑、数据模型、Compose UI
  • androidMain — 放Android平台特有的实现(比如用Android的Context
  • iosMain — 放iOS平台特有的实现(比如调用UIKit

一个典型的配置长这样:

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions {
                jvmTarget = "17"
            }
        }
    }
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
    }

    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation(compose.runtime)
                implementation(compose.foundation)
                implementation(compose.material3)
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("androidx.activity:activity-compose:1.8.2")
            }
        }
        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)
        }
    }
}
💡 小技巧: iOS的多个架构(x64、arm64、simulator)可以合并成一个iosMain sourceSet,这样你只需要写一份iOS平台代码,Gradle会自动为每个架构编译。

共享代码:expect/actual 模式

共享代码是KMP最吸引人的地方。但现实是,有些API在Android和iOS上完全不同。比如获取当前平台名称,Android用Build.MODEL,iOS用UIDevice.currentDevice.systemName

这时候就用expectactual关键字。你在commonMain里声明一个expect函数,然后在各平台sourceSet里提供actual实现。

// commonMain
expect fun getPlatformName(): String

// androidMain
actual fun getPlatformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"

// iosMain
actual fun getPlatformName(): String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion

嗯,这里要注意:expectactual的签名必须完全一致,包括参数和返回类型。我刚开始写的时候经常漏掉actual关键字,编译直接报错。

平台特定依赖:别把Android的库塞到iOS里

KMP里最忌讳的就是把平台特定的依赖放到commonMain里。比如androidx.lifecycle只能在Android上用,你放到commonMain里,iOS编译直接挂。

正确的做法是:

  • 通用依赖(如Ktor、kotlinx.serialization)放commonMain
  • Android专用依赖(如androidx.*)放androidMain
  • iOS专用依赖(如platform.Foundation)放iosMain

举个例子:

sourceSets {
    val commonMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-core:2.3.7")
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
        }
    }
    val androidMain by getting {
        dependencies {
            implementation("io.ktor:ktor-client-okhttp:2.3.7")
            implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")
        }
    }
    val iosMain by creating {
        dependencies {
            implementation("io.ktor:ktor-client-darwin:2.3.7")
        }
    }
}
🔑 核心原则: commonMain只放纯Kotlin代码和跨平台库。任何带android.java.UIKit前缀的类,都不能出现在commonMain里。

发布KMP库:让全世界用你的代码

写好了KMP库,怎么发布到Maven Central或私有仓库?这里我踩过不少坑。最核心的一点是:KMP库需要为每个目标平台生成对应的artifact。

配置发布脚本,我推荐用maven-publish插件配合signing插件:

plugins {
    `maven-publish`
    signing
}

publishing {
    publications {
        withType<MavenPublication> {
            pom {
                name.set("MyKMPLibrary")
                description.set("A Kotlin Multiplatform library for Compose")
                url.set("https://github.com/your/repo")
                licenses {
                    license {
                        name.set("Apache-2.0")
                        url.set("https://opensource.org/licenses/Apache-2.0")
                    }
                }
                developers {
                    developer {
                        id.set("your-id")
                        name.set("Your Name")
                        email.set("your@email.com")
                    }
                }
            }
        }
    }
    repositories {
        maven {
            url = uri("https://s01.oss.sonatype.org/service/local/staging/deploy/maven2/")
            credentials {
                username = project.findProperty("ossrhUsername") as String?
                password = project.findProperty("ossrhPassword") as String?
            }
        }
    }
}

发布前记得检查:

  • 所有目标平台的artifact是否都生成了(Android的.aar、iOS的.framework)
  • POM文件是否包含了所有依赖信息
  • 签名是否配置正确(Maven Central要求PGP签名)
⚠️ 避坑指南: 我曾经发布一个KMP库,结果iOS的framework没有包含iosSimulatorArm64架构。用户在M1芯片的Mac上跑模拟器时直接crash。后来我在binaries.framework里加上了iosSimulatorArm64()才解决。所以发布前一定要在模拟器和真机上各测一遍。

好了,关于Compose与KMP的配置、共享代码、依赖管理和发布,基本就这些。实际项目中你可能会遇到更复杂的情况,比如多模块间的依赖传递、CocoaPods集成等。但核心思路不变:commonMain写逻辑,平台sourceSet写实现,依赖按需隔离。记住这个原则,KMP项目就不会跑偏。


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