21、Compose与Kotlin Multiplatform:Compose编译器配置、KMP模块配置、共享代码、平台特定依赖、发布KMP库
聊到Kotlin Multiplatform(简称KMP),我其实挺感慨的。早些年做跨平台,要么用Flutter,要么上RN,Kotlin这边一直差点意思。直到JetBrains把Compose Multiplatform推出来,我才觉得「嗯,这才是Kotlin该有的样子」。说白了,KMP让你用一套代码写UI逻辑,同时还能调用各平台的原生API。今天我们就把它拆开揉碎了讲清楚。
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)
}
}
}
}
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)
}
}
}
iosMain sourceSet,这样你只需要写一份iOS平台代码,Gradle会自动为每个架构编译。
共享代码:expect/actual 模式
共享代码是KMP最吸引人的地方。但现实是,有些API在Android和iOS上完全不同。比如获取当前平台名称,Android用Build.MODEL,iOS用UIDevice.currentDevice.systemName。
这时候就用expect和actual关键字。你在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
嗯,这里要注意:expect和actual的签名必须完全一致,包括参数和返回类型。我刚开始写的时候经常漏掉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")
}
}
}
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签名)
iosSimulatorArm64架构。用户在M1芯片的Mac上跑模拟器时直接crash。后来我在binaries.framework里加上了iosSimulatorArm64()才解决。所以发布前一定要在模拟器和真机上各测一遍。
好了,关于Compose与KMP的配置、共享代码、依赖管理和发布,基本就这些。实际项目中你可能会遇到更复杂的情况,比如多模块间的依赖传递、CocoaPods集成等。但核心思路不变:commonMain写逻辑,平台sourceSet写实现,依赖按需隔离。记住这个原则,KMP项目就不会跑偏。
公众号:蓝海资料掘金营,微信deep3321