llms.txt: machine-readable documentation index. Markdown version of this page is also available.

Kotlin 语言参考文档 中文版 Help

跨平台项目结构的高级概念

本文解释 Kotlin Multiplatform 项目结构的高级概念, 以及如何对应到 Gradle 实现. 如果你需要使用 Gradle 构建的低层抽象(配置, 任务, 发布, 等等), 或者正在为 Kotlin Multiplatform 构建创建 Gradle plugin, 那么这些信息会对你很有用.

如果你正在进行下面的工作, 本章会对你很有用:

  • 需要在一组特定的编译目标之间共用代码, 但 Kotlin 默认不会为这些编译目标创建源代码集.

  • 想要为 Kotlin Multiplatform 构建创建 Gradle plugin, 或者需要使用 Gradle 构建的低层抽象, 例如配置, 任务, 发布, 等等.

要理解跨平台项目中的依赖项管理, 很关键的一点是对 Gradle 风格项目或库的依赖项, 和 Kotlin 特有的源代码集之间的 dependsOn 关系之间的区别:

  • dependsOn 是共通源代码集和平台相关源代码集之间的关系, 一般来说, 这种关系可以组成 源代码集层级关系, 并在跨平台项目中共用代码. 对于默认的源代码集, 会自动管理层级关系, 但在特定的情况下, 你也可能需要调整它.

  • 对库和项目的依赖项一般来说按照通常的方式工作, 但要在跨平台项目中正确的管理它们, 你需要理解 Gradle 依赖项如何解析 成为细粒度的、用于编译的 源代码集 -> 源代码集 依赖项.

dependsOn 与源代码集层级关系

通常, 你会使用 依赖项 而不是 dependsOn 关系. 但是, 为了理解 Kotlin Multiplatform 项目的底层工作方式, 研究 dependsOn 是至关重要的.

dependsOn 是 2 个 Kotlin 源代码集之间的, Kotlin 特有的关系. 它可以是共通源代码集与平台相关源代码集之间的连接, 例如, 可以表示 jvmMain 源代码集依赖于 commonMain, iosArm64Main 依赖于 iosMain, 等等.

考虑一个一般的例子, 存在 Kotlin 源代码集 A 和 B. 表达式 A.dependsOn(B) 会指示 Kotlin:

  1. A 可以看到来自 B 的 API, 包括内部声明.

  2. A 可以为来自 B 的预期声明提供实际实现. 这是一个必须而且充分的条件, 因为, 当而且仅当, 存在直接或间接的 A.dependsOn(B) 关系时, A 才可以为 B 提供 actuals 实现.

  3. B 应该编译到 A 编译到的所有编译目标, 此外再加上它自己的编译目标.

  4. A 继承 B 的所有的常规依赖项.

dependsOn 关系会创建一个树形结构, 也叫做源代码集层级. 下面是一个通常的移动开发项目的例子, 针对的目标平台是 android, iosArm64 (iPhone 设备), 和 iosSimulatorArm64 (Apple Silicon Mac 上的 iPhone 模拟器):

DependsOn 关系树结构

图中的箭头表示 dependsOn 关系. 在编译平台二进制文件时, 也会保持这些关系. 所以 Kotlin 能够理解, iosMain 应该能够看到来自 commonMain 的 API, 但不能看到来自 iosArm64Main 的 API:

编译期间的 DependsOn 关系

dependsOn 关系使用 KotlinSourceSet.dependsOn(KotlinSourceSet) 调用来配置, 例如:

kotlin { // 编译目标的声明 sourceSets { // 配置 dependsOn 关系的示例 iosArm64Main.dependsOn(commonMain) } }
  • 这个示例演示如何在构建脚本中定义 dependsOn 关系. 但是, Kotlin Gradle plugin 会默认创建源代码集并设置它们的关系, 因此你不需要手动配置.

  • 在构建脚本中, dependsOn 关系在 dependencies {} 代码块之外的地方声明. 这是因为 dependsOn 不是一种通常的依赖项; 相反, 它是 Kotlin 源代码集之间的一种特别关系, 在不同的编译目标之间共用代码时需要这些依赖关系.

你不能使用 dependsOn 来声明对已发布的库或对另一个 Gradle 项目的通常的依赖项. 例如, 你不能设置 commonMain 依赖于 kotlinx-coroutines-core 库的 commonMain, 也不能调用 commonTest.dependsOn(commonMain).

声明自定义的源代码集

有些情况下, 在你的项目中可能需要自定义的中间源代码集. 考虑一个项目, 编译到 JVM, JS, 和 Linux 平台, 你想要只在 JVM 和 JS 平台之间共用一些源代码. 这种情况下, 你应该为这组编译目标寻找一个特定的源代码集, 具体方法请参见 跨平台项目结构的基础知识.

Kotlin 不会自动创建这样的源代码集. 因此你应该使用 by creating 构造来手动创建它:

kotlin { jvm() js() linuxX64() sourceSets { // 创建名为 "jvmAndJs" 的源代码集 val jvmAndJsMain by creating { // ... } } }

但是, Kotlin 仍然如何处理或者编译这个源代码集. 如果你画一个源代码集关系图, 这个源代码集将是孤立的, 没有添加任何编译目标的标签:

dependsOn 关系缺失

为了解决这个问题, 请添加几个 dependsOn 关系, 将 jvmAndJsMain 包含到层级结构中:

kotlin { jvm() js() linuxX64() sourceSets { val jvmAndJsMain by creating { // 不要忘记添加对 commonMain 的 dependsOn dependsOn(commonMain.get()) } jvmMain { dependsOn(jvmAndJsMain) } jsMain { dependsOn(jvmAndJsMain) } } }

这里, jvmMain.dependsOn(jvmAndJsMain) 会对 jvmAndJsMain 添加 JVM 编译目标, jsMain.dependsOn(jvmAndJsMain) 会对 jvmAndJsMain 添加 JS 编译目标.

最终的项目结构如下:

最终的项目结构

对其他库或项目的依赖

在跨平台项目中, 你可以设置通常的依赖项, 可以依赖到已发布的库, 或依赖到另一个 Gradle 项目.

Kotlin Multiplatform 一般会通过通常的 Gradle 方式来声明依赖项. 与 Gradle 类似, 你应该:

  • 在你的构建脚本中使用 dependencies {} 代码块.

  • 为依赖项选择适当的范围(scope), 例如, implementation 或 api.

  • 引用依赖项, 如果它是已经发布到仓库中, 可以指定它的座标(coordinate), 例如 "com.google.guava:guava:32.1.2-jre", 如果它是同一个构建内的一个 Gradle 项目, 可以指定它的路径, 例如 project(":utils:concurrency").

跨平台项目中的依赖项配置有一些特别的功能. 每个 Kotlin 源代码集有它独自的 dependencies {} 代码块. 因此你可以在平台相关的源代码集中声明平台相关的依赖项:

kotlin { // 编译目标的声明 sourceSets { jvmMain.dependencies { // 这是 jvmMain 的依赖项, 因此可以添加 JVM 相关的依赖项 implementation("com.google.guava:guava:32.1.2-jre") } } }

共通的依赖项要复杂一些. 考虑一个跨平台项目, 声明了对一个跨平台库的依赖项, 例如, kotlinx.coroutines:

kotlin { android() // Android iosArm64() // iPhone 设备 iosSimulatorArm64() // Apple Silicon Mac 上的 iPhone 模拟器 sourceSets { commonMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } } }

在依赖解析中, 有 3 个重要概念:

  1. 跨平台依赖项会沿着 dependsOn 结构向下传播. 如果你对 commonMain 添加一个依赖项, 它会自动添加到声明了对 commonMain 直接或间接的 dependsOn 关系的所有源代码集.

    在这个例子中, 依赖项实际会被自动添加到所有的 *Main 源代码集: iosMain, jvmMain, iosSimulatorArm64Main, 和 iosArm64Main. 所有这些源代码集会从 commonMain 源代码集继承 kotlin-coroutines-core 依赖项, 因此你不需要将依赖项手动的复制粘贴到这些源代码集中:

    跨平台依赖项的传播

  2. 源代码集 -> 跨平台库 依赖项, 例如上面的 commonMain 对 org.jetbrians.kotlinx:kotlinx-coroutines-core:1.7.3 的依赖, 表示依赖解析的中间状态. 解析的最终状态始终表示为 源代码集 -> 源代码集 依赖项.

    为了推断细粒度的 源代码集 -> 源代码集 依赖项, Kotlin 会读取和每个跨平台库一起发布的源代码集结构. 完成这一步之后, 每个库的内部表达不是一个整体, 而是它的源代码集的集合. 请看 kotlinx-coroutines-core 的例子:

    源代码集结构的序列化
  3. Kotlin 对每个依赖关系解析为依赖项中源代码集的集合. 这个集合中的每个依赖项源代码集必须拥有 兼容的编译目标. 依赖项源代码集拥有兼容的编译目标是指, 它至少编译到 与使用它的源代码集相同的编译目标.

    例如, 示例项目中的 commonMain 编译到 android, iosArm64, 和 iosSimulatorArm64:

    • 首先, 它解析到一个对 kotlinx-coroutines-core.commonMain 的依赖项. 因为 kotlinx-coroutines-core 编译到所有可能的 Kotlin 编译目标. 因此, 它的 commonMain 会编译到所有可能的编译目标, 包括这里要求的 android, iosArm64, 和 iosSimulatorArm64.

    • 其次, commonMain 依赖 kotlinx-coroutines-core.concurrentMain. 因为 kotlinx-coroutines-core 中的 concurrentMain 编译到除 JS 之外的所有的编译目标, 它匹配使用它的项目中的 commonMain 的编译目标.

    但是, coroutines 中的 iosArm64Main 之类的源代码集, 不兼容于使用它的 commonMain 源代码集. 即使 iosArm64Main 编译到 commonMain 的编译目标之一, 也就是, iosArm64, 但是它不编译到 android 或 iosSimulatorArm64.

    依赖解析的结果直接影响可以访问 kotlinx-coroutines-core 中的哪些代码:

    在共通代码中使用 JVM 专用 API 的错误

对齐跨源代码集的共通依赖项的版本

在 Kotlin Multiplatform 项目中, 共通源代码集会被编译多次, 生成 klib, 并成为配置的每个 编译 的一部分. 为了生成一致的二进制文件, 共通代码每次编译时, 应该使用跨平台依赖项的相同版本. Kotlin Gradle plugin 会帮助我们对齐这些依赖项, 确保每个源代码集的有效依赖项版本都是相同的.

在上面的示例中, 想象一下, 如果你想要向 androidMain 源代码集添加 androidx.navigation:navigation-compose:2.7.7 依赖项. 你的项目为 commonMain 源代码集明确的声明了 kotlinx-coroutines-core:1.7.3 依赖项, 但 Compose Navigation 库的 2.7.7 版本需要 Kotlin coroutines 的 1.8.0 或更高版本.

由于 commonMain 和 androidMain 会一起编译, Kotlin Gradle plugin 会在 coroutines 库的两个版本之间进行选择, 并对 commonMain 源代码集使用 kotlinx-coroutines-core:1.8.0. 但为了让共通代码对配置的所有编译目标都一致的编译, iOS 源代码集也需要限定为相同的依赖项版本. 因此 Gradle 还会将 kotlinx.coroutines-*:1.8.0 依赖项传播给 iosMain 源代码集.

在 *Main 源代码集之间对齐依赖项

对于 *Main 源代码集和 *Test 源代码集, 依赖项会分别对齐. 对 *Test 源代码集的 Gradle 配置包含 *Main 源代码集的所有依赖项, 但反过来不是如此. 因此你可以使用比较新的库版本来测试你的项目, 而不会影响你的主代码.

例如, 在你的 *Main 源代码集存在 Kotlin coroutines 1.7.3 的依赖项, 传播到项目中的每个源代码集. 但是, 在 iosTest 源代码集中, 你决定将版本更新到 1.8.0, 来测试这个库的新发布版. 根据相同的算法, 这个依赖项会通过 *Test 源代码集树传播, 因此每个 *Test 源代码集都会使用 kotlinx.coroutines-*:1.8.0 依赖项进行编译.

Test 源代码集与 main 源代码集会分别解析依赖项

编译

与单一平台的项目不同, Kotlin Multiplatform 项目需要多次编译器运行来构建所有的 artifact. 每次编译器运行都是一个 Kotlin 编译.

例如, 在前面提到的 Kotlin 编译过程中, 用于 iPhone 设备的二进制文件的生成方式如下:

针对 iOS 的 Kotlin 编译

Kotlin 编译会在编译目标之下分组. 默认情况下, Kotlin 为每个编译目标创建 2 个编译, main 编译用于产品源代码, test 编译用于测试源代码.

在构建脚本中, 编译通过类似的方式访问. 你首先选择一个 Kotlin 编译目标, 然后访问其中的 compilations 容器, 最后通过名称选择你需要的编译:

kotlin { // 声明并配置 JVM 编译目标 jvm { val mainCompilation: KotlinJvmCompilation = compilations.getByName("main") } }
2026/10/11