3. KMP项目结构解析
KMP 工程最容易让人“迷路”的地方,不是 Kotlin 语法,而是目录与 Gradle 脚本之间的对应关系:哪个目录会被编译进哪个平台、依赖应该写在哪一层、为什么 commonMain 里不能直接调用 Android 的 Context。理解这套结构,等于拿到了跨端工程的“地图”。本节以典型的 KMP 工程为例,逐层拆解 settings.gradle.kts、根 build.gradle.kts、模块 build.gradle.kts 以及各 sourceSet 目录的职责与依赖声明方式。
一、settings.gradle.kts:工程边界与模块清单
settings.gradle.kts 是 Gradle 构建的入口,它决定“这次构建包含哪些工程”。在 KMP 中它通常承担三件事:声明插件仓库与依赖仓库、定义工程名、通过 include 引入子模块。一个常见的写法如下:
// settings.gradle.kts
pluginManagement {
repositories {
google()
gradlePluginPortal()
mavenCentral()
}
}
dependencyResolutionManagement {
repositories {
google()
mavenCentral()
}
}
rootProject.name = "KmpDemo"
include(":shared")
include(":androidApp")
include(":desktopApp")
include(":webApp")
这里的关键点是:shared 是承载跨端公共逻辑的 KMP 模块,而 androidApp、desktopApp、webApp 是各平台的应用壳工程。iOS 端通常不通过 Gradle 的 include 引入,而是由 Xcode 工程通过 Gradle 生成的 framework 或 XCFramework 来消费 shared,因此你会在 settings 中看不到 iOS 应用模块,这是正常现象。若把仓库配置写在 settings 的 dependencyResolutionManagement 中,子模块就无需重复声明仓库,能避免多模块仓库配置不一致的问题。
二、根 build.gradle.kts:插件版本与全局约定
根 build.gradle.kts 一般不写具体业务依赖,而是用 plugins 块配合 apply false 统一声明插件版本,供子模块按需启用:
// 根 build.gradle.kts
plugins {
kotlin("multiplatform") version "2.0.20" apply false
kotlin("plugin.serialization") version "2.0.20" apply false
id("com.android.library") version "8.5.2" apply false
id("com.android.application") version "8.5.2" apply false
id("org.jetbrains.compose") version "1.7.0" apply false
}
这样做的好处是版本集中管理:子模块只写 kotlin("multiplatform") 而不带版本号,升级时只改根文件一处。若项目使用版本目录,还会出现 gradle/libs.versions.toml,把版本号与库坐标抽成别名,根脚本通过 alias(libs.plugins.kotlinMultiplatform) 引用,进一步降低重复。
三、模块 build.gradle.kts:target 与 sourceSet 的声明中心
真正定义“支持哪些平台、每个平台有哪些源集、各自依赖什么”的,是 shared/build.gradle.kts。它的骨架由 kotlin { } 块构成,内部先声明 target,再配置 sourceSets:
// shared/build.gradle.kts
plugins {
kotlin("multiplatform")
kotlin("plugin.serialization")
id("com.android.library")
}
kotlin {
androidTarget()
jvm("desktop")
iosX64(); iosArm64(); iosSimulatorArm64()
js(IR) { browser() }
sourceSets {
val commonMain by getting {
dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("io.ktor:ktor-client-core:2.3.12")
}
}
val commonTest by getting {
dependencies { implementation(kotlin("test")) }
}
val androidMain by getting {
dependencies { implementation("io.ktor:ktor-client-okhttp:2.3.12") }
}
val iosMain by creating {
dependsOn(commonMain)
dependencies { implementation("io.ktor:ktor-client-darwin:2.3.12") }
}
val desktopMain by getting {
dependencies { implementation("io.ktor:ktor-client-cio:2.3.12") }
}
val jsMain by getting {
dependencies { implementation("io.ktor:ktor-client-js:2.3.12") }
}
}
}
这段配置体现了 KMP 依赖声明的核心原则:公共依赖放 commonMain,平台专属依赖放对应平台源集。例如 Ktor 的引擎必须按平台选择——Android 用 OkHttp、iOS 用 Darwin、JVM 桌面用 CIO、JS 用 JS 引擎,而 ktor-client-core 这类纯公共 API 则放在 commonMain。若把 OkHttp 写进 commonMain,编译期就会因 Android 专属 API 无法在其他平台解析而失败。
iosMain 中间源集,它会自动 dependsOn(commonMain),无需手动创建。若手动 creating 一个 iosMain,务必显式声明 dependsOn(commonMain),否则公共代码不可见。四、sourceSets 目录职责与可见性规则
每个 sourceSet 对应磁盘上一个同名目录,默认位于 src/<sourceSetName>/kotlin。它们的职责边界如下:
- commonMain:存放所有平台共享的业务逻辑、数据模型、接口定义、网络请求封装等。只能使用 Kotlin 标准库与跨平台库,不能引用
android.*、platform.Foundation.*等平台 API。 - androidMain:Android 专属实现,可访问
Context、SharedPreferences、OkHttp 等。通常用于实现 commonMain 中定义的expect声明。 - iosMain:iOS 专属实现,可调用 Foundation、UIKit 等,通过 cinterop 或 Kotlin/Native 提供的平台库访问。
- jvmMain(示例中命名为 desktopMain):JVM 桌面端实现,可使用 Java 标准库、Swing/Compose Desktop 等。
- jsMain:Kotlin/JS 实现,可访问浏览器 DOM API 或 Node.js API。
可见性方向是单向的:平台源集可以看到 commonMain 的内容,反之不行。因此当公共逻辑需要平台能力时,使用 expect/actual 机制:在 commonMain 写 expect fun platformName(): String,在各平台源集写 actual fun platformName(): String。这样 commonMain 只依赖抽象声明,具体实现由平台源集提供,编译时按 target 匹配。
五、依赖声明中的常见陷阱与判断依据
实际配置时,最容易出问题的是“依赖放错层”。判断方法很简单:问自己“这个库是否在所有目标平台都有对应产物”。如果只有 Android 版本,就必须放 androidMain;如果所有平台都有(如 kotlinx-coroutines-core),才放 commonMain。另一个常见问题是 implementation 与 api 的选择:若某个依赖的类型出现在公共 API 签名中(例如 commonMain 中公开函数返回 HttpClient),应使用 api,否则下游模块编译时找不到该类型;若仅内部使用,用 implementation 即可,能减少传递依赖暴露。
此外,Android target 需要额外配置 android { } 块,指定 namespace、compileSdk、minSdk 等;iOS target 若需要生成 framework,还要在 kotlin { } 中配置 binaries.framework { baseName = "shared"; isStatic = true },Xcode 才能正确链接。这些配置与 sourceSet 共同构成 KMP 模块的完整声明。
Could not resolve,且错误信息往往指向 commonMain,容易误导排查方向。正确做法是先确认库的多平台支持矩阵,再决定放置层级。