Kotlin Multiplatform 项目结构基础
使用 Kotlin Multiplatform,您可以在不同平台之间共享代码。本文将解释共享代码的约束、如何区分代码中的共享部分和平台特有部分,以及如何指定此共享代码所适用的平台。
您还将学习 Kotlin Multiplatform 项目设置的核心概念,例如共同代码、目标、平台特有和中间源代码集,以及测试集成。这将有助于您将来设置您的多平台项目。
此处介绍的模型相较于 Kotlin 所使用的模型有所简化。然而,这个基本模型应该足以应对大多数情况。
共同代码
共同代码 是在不同平台之间共享的 Kotlin 代码。
考虑一个简单的“Hello, World”示例:
fun greeting() {
println("Hello, Kotlin Multiplatform!")
}在平台之间共享的 Kotlin 代码通常位于 commonMain 目录中。代码文件的位置很重要,因为它会影响此代码将被编译到的平台列表。
Kotlin 编译器接收源代码作为输入,并生成一组平台特有的二进制文件。在编译多平台项目时,它可以从相同的代码生成多个二进制文件。例如,编译器可以从同一个 Kotlin 文件生成 JVM .class 文件和原生可执行文件:
并非所有 Kotlin 代码都可以编译到所有平台。Kotlin 编译器会阻止您在共同代码中使用平台特有函数或类,因为此代码无法编译到不同的平台。
例如,您不能在共同代码中使用 java.io.File 依赖项。它是 JDK 的一部分,而共同代码也会被编译为原生代码,在该环境中,JDK 类不可用:

在共同代码中,您可以使用 Kotlin Multiplatform 库。这些库提供了一个共同的 API,可以在不同平台以不同方式实现。在这种情况下,平台特有的 API 充当额外部分,尝试在共同代码中使用此类 API 会导致错误。
例如,kotlinx.coroutines 是一个支持所有目标的 Kotlin Multiplatform 库,但它也有一个平台特有的部分,可将 kotlinx.coroutines 并发原语转换为 JDK 并发原语,例如 fun CoroutinesDispatcher.asExecutor(): Executor。API 的此附加部分在 commonMain 中不可用。
目标
目标定义了 Kotlin 将共同代码编译到的平台。例如,这些可以是 JVM、JS、Android、iOS 或 Linux。前面的示例将共同代码编译到了 JVM 和原生目标。
Kotlin 目标 是一个描述编译目标的标识符。它定义了生成二进制文件的格式、可用的语言构造和允许的依赖项。
目标也可以被称为平台。有关完整支持的目标列表,请参见。
您应该首先_声明_一个目标,以指示 Kotlin 为该特定目标编译代码。在 Gradle 中,您可以在 kotlin {} 代码块中使用预定义的 DSL 调用来声明目标:
kotlin {
jvm() // 声明一个 JVM 目标
iosArm64() // 声明一个对应于 64 位 iPhone 的目标
}这样,每个多平台项目都定义了一组支持的目标。关于在构建脚本中声明目标的更多信息,请参见分层项目结构部分。
声明了 jvm 和 iosArm64 目标后,commonMain 中的共同代码将被编译到这些目标:
为了理解哪些代码将被编译到特定目标,您可以将目标视为附加到 Kotlin 源代码文件上的标签。Kotlin 使用这些标签来确定如何编译您的代码、生成哪些二进制文件以及该代码中允许使用哪些语言构造和依赖项。
如果您也想将 greeting.kt 文件编译到 .js,您只需声明 JS 目标。然后,commonMain 中的代码会收到一个额外的 js 标签,对应于 JS 目标,指示 Kotlin 生成 .js 文件:
这就是 Kotlin 编译器处理编译到所有已声明目标的共同代码的方式。关于如何编写平台特有代码,请参见源代码集。
源代码集
Kotlin 源代码集 是一组源代码文件,拥有自己的目标、依赖项和编译器选项。它是多平台项目中共享代码的主要方式。
多平台项目中的每个源代码集:
- 具有在给定项目中唯一的名称。
- 包含一组源代码文件和资源,通常存储在以源代码集名称命名的目录中。
- 指定此源代码集中的代码将编译到的一组目标。这些目标会影响此源代码集中可用的语言构造和依赖项。
- 定义自己的依赖项和编译器选项。
Kotlin 提供了一系列预定义的源代码集。其中之一是 commonMain,它存在于所有多平台项目中,并编译到所有已声明的目标。
在 Kotlin Multiplatform 项目中,您将源代码集作为 src 内部的目录进行交互。例如,一个包含 commonMain、iosMain 和 jvmMain 源代码集的项目具有以下结构:

在 Gradle 脚本中,您可以通过名称在 kotlin.sourceSets {} 代码块中访问源代码集:
kotlin {
// 目标声明:
// …
// 源代码集声明:
sourceSets {
commonMain {
// 配置 commonMain 源代码集
}
}
}除了 commonMain 之外,其他源代码集可以是平台特有或中间的。
平台特有源代码集
尽管只有共同代码很方便,但并非总是可行。commonMain 中的代码会编译到所有已声明的目标,并且 Kotlin 不允许您在那里使用任何平台特有的 API。
在一个包含原生和 JS 目标的多平台项目中,commonMain 中的以下代码无法编译:
// commonMain/kotlin/common.kt
// 无法在共同代码中编译
fun greeting() {
java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}作为解决方案,Kotlin 会创建平台特有源代码集,也称为平台源代码集。每个目标都有一个对应的平台源代码集,仅为该目标进行编译。例如,jvm 目标具有对应的 jvmMain 源代码集,仅编译到 JVM。Kotlin 允许在这些源代码集使用平台特有依赖项,例如在 jvmMain 中使用 JDK:
// jvmMain/kotlin/jvm.kt
// 您可以在 `jvmMain` 源代码集中使用 Java 依赖项
fun jvmGreeting() {
java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}编译到特定目标
编译到特定目标涉及多个源代码集。当 Kotlin 将多平台项目编译到特定目标时,它会收集所有标记有该目标的源代码集,并从中生成二进制文件。
考虑一个包含 jvm、iosArm64 和 js 目标的示例。Kotlin 会为共同代码创建 commonMain 源代码集,并为特定目标创建对应的 jvmMain、iosArm64Main 和 jsMain 源代码集:
在编译到 JVM 期间,Kotlin 会选择所有标记有“JVM”的源代码集,即 jvmMain 和 commonMain。然后它将它们一起编译为 JVM 类文件:
由于 Kotlin 将 commonMain 和 jvmMain 一起编译,因此生成的二进制文件包含来自 commonMain 和 jvmMain 的声明。
在使用多平台项目时,请记住:
- 如果您希望 Kotlin 将您的代码编译到特定平台,请声明一个对应的目标。
- 要选择存储代码的目录或源文件,请首先决定您希望在哪些目标之间共享代码:
- 如果代码在所有目标之间共享,则应在
commonMain中声明。 - 如果代码仅用于一个目标,则应在该目标的平台特有源代码集中定义(例如,JVM 的
jvmMain)。
- 如果代码在所有目标之间共享,则应在
- 在平台特有源代码集中编写的代码可以访问共同源代码集中的声明。例如,
jvmMain中的代码可以使用commonMain中的代码。然而,反之则不成立:commonMain不能使用jvmMain中的代码。 - 在平台特有源代码集中编写的代码可以使用对应的平台依赖项。例如,
jvmMain中的代码可以使用仅限 Java 的库,例如 Guava 或 Spring。
中间源代码集
简单的多平台项目通常只包含共同代码和平台特有代码。commonMain 源代码集代表在所有已声明目标之间共享的共同代码。平台特有源代码集,例如 jvmMain,代表仅编译到相应目标的平台特有代码。
实际上,您通常需要更细粒度的代码共享。
考虑一个您需要面向所有现代 Apple 设备和 Android 设备的示例:
kotlin {
androidTarget()
iosArm64() // 64 位 iPhone 设备
macosArm64() // 基于现代 Apple 芯片的 Mac
watchosX64() // 现代 64 位 Apple Watch 设备
tvosArm64() // 现代 Apple TV 设备
}您需要一个源代码集来添加一个为所有 Apple 设备生成 UUID 的函数:
import platform.Foundation.NSUUID
fun randomUuidString(): String {
// 您希望访问 Apple 特有的 API
return NSUUID().UUIDString()
}您不能将此函数添加到 commonMain 中。commonMain 会编译到所有已声明的目标,包括 Android,但 platform.Foundation.NSUUID 是一个 Apple 特有的 API,在 Android 上不可用。如果您尝试在 commonMain 中引用 NSUUID,Kotlin 会显示错误。
您可以将此代码复制并粘贴到每个 Apple 特有源代码集:iosArm64Main、macosArm64Main、watchosX64Main 和 tvosArm64Main 中。但这种方法不推荐,因为像这样重复代码容易出错。
为了解决这个问题,您可以使用_中间源代码集_。中间源代码集是一个 Kotlin 源代码集,它编译到项目中的部分目标,但不是全部目标。您也可以看到中间源代码集被称为分层源代码集或简称层次结构。
Kotlin 默认创建一些中间源代码集。在这种特定情况下,生成的项目结构将如下所示:
此处,底部的多色块是平台特有源代码集。为清晰起见,省略了目标标签。
appleMain 代码块是 Kotlin 创建的一个中间源代码集,用于共享编译到 Apple 特有目标的代码。appleMain 源代码集仅编译到 Apple 目标。因此,Kotlin 允许在 appleMain 中使用 Apple 特有 API,您可以在此处添加 randomUUID() 函数。
关于查找 Kotlin 默认创建和设置的所有中间源代码集,以及如果 Kotlin 默认不提供您所需的中间源代码集时应如何操作,请参见分层项目结构。
在编译到特定目标期间,Kotlin 会获取所有标记有此目标的源代码集,包括中间源代码集。因此,在编译到 iosArm64 平台目标时,commonMain、appleMain 和 iosArm64Main 源代码集中编写的所有代码都会被组合。
如果某些源代码集没有源代码,这也没关系。例如,在 iOS 开发中,通常不需要提供仅适用于 iOS 设备但不适用于 iOS 模拟器的代码。因此,
iosArm64Main很少使用。
Apple 设备和模拟器目标
当您使用 Kotlin Multiplatform 开发 iOS 移动应用程序时,您通常会使用 iosMain 源代码集。尽管您可能认为它是 ios 目标的平台特有源代码集,但并没有单一的 ios 目标。大多数移动项目至少需要两个目标:
- 设备目标 用于生成可在 iOS 设备上执行的二进制文件。目前 iOS 只有一个设备目标:
iosArm64。 - 模拟器目标 用于生成在您机器上启动的 iOS 模拟器所需的二进制文件。如果您有基于 Apple 芯片的 Mac 电脑,请选择
iosSimulatorArm64作为模拟器目标。如果您有基于 Intel 的 Mac 电脑,请使用iosX64。
如果您只声明 iosArm64 设备目标,您将无法在本地机器上运行和调试您的应用程序和测试。
平台特有源代码集,例如 iosArm64Main、iosSimulatorArm64Main 和 iosX64Main 通常是空的,因为用于 iOS 设备和模拟器的 Kotlin 代码通常是相同的。您可以使用 iosMain 中间源代码集在它们之间共享代码。
同样适用于其他非 Mac 的 Apple 目标。例如,如果您有用于 Apple TV 的 tvosArm64 设备目标,以及分别用于基于 Apple 芯片和 Intel 设备的 Apple TV 模拟器的 tvosSimulatorArm64 和 tvosX64 模拟器目标,您可以使用 tvosMain 中间源代码集来共享所有这些目标的代码。
测试集成
实际项目除了主要的生产代码外,还需要测试。这就是为什么所有默认创建的源代码集都带有 Main 和 Test 后缀的原因。Main 包含生产代码,而 Test 包含此代码的测试。它们之间的连接是自动建立的,测试可以在无需额外配置的情况下使用 Main 代码提供的 API。
Test 对应部分也是类似于 Main 的源代码集。例如,commonTest 是 commonMain 的对应部分,并编译到所有已声明的目标,允许您编写共同测试。平台特有测试源代码集,例如 jvmTest,用于编写平台特有测试,例如 JVM 特有测试或需要 JVM API 的测试。
除了拥有一个用于编写共同测试的源代码集之外,您还需要一个多平台测试框架。Kotlin 提供了一个默认的 kotlin.test 库,它带有 @kotlin.Test 注解和各种断言方法,例如 assertEquals 和 assertTrue。
您可以在各自的源代码集中为每个平台编写平台特有测试,就像常规测试一样。与主代码类似,您可以为每个源代码集设置平台特有依赖项,例如 JVM 的 JUnit 和 iOS 的 XCTest。要为特定目标运行测试,请使用 <targetName>Test 任务。
有关如何创建和运行多平台测试,请参阅测试您的多平台应用教程。
