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

在通用代码中,您可以使用 Kotlin 多平台库。这些库提供了一个通用的 API,可以在不同的平台上以不同的方式实现。在这种情况下,特定于平台的 API 就像额外的组件,尝试在通用代码中使用这些 API 会导致错误。
例如,kotlinx.coroutines是一个支持所有目标平台的 Kotlin 多平台库,但它也包含一个平台特定的部分,用于将kotlinx.coroutines并发原语转换为 JDK 并发原语,例如fun CoroutinesDispatcher.asExecutor(): Executor。API 的这一附加部分在中不可用commonMain。
要探索可用的 Kotlin 多平台库,请访问klibs.io。
目标
目标定义了 Kotlin 将通用代码编译到的平台。例如,这些平台可以是 JVM、JS、Android、iOS 或 Linux。前面的示例将通用代码编译到了 JVM 和原生平台。
Kotlin目标是描述编译目标的标识符。它定义了生成的二进制文件的格式、可用的语言结构以及允许的依赖项。
目标设备也可以称为平台。请参阅支持的目标设备完整列表。
首先,您应该声明一个目标,以指示 Kotlin 为该特定目标编译代码。在 Gradle 中,您可以使用预定义的 DSL 调用在kotlin {}代码块内声明目标:
kotlin {
jvm() // Declares a JVM target
iosArm64() // Declares a target that corresponds to 64-bit iPhones
}这样,每个多平台项目都定义了一组受支持的目标平台。请参阅“分层项目结构”部分,了解有关在构建脚本中声明目标平台的更多信息。
jvm声明了 and targets之后iosArm64public code 中的通用代码commonMain将被编译为这些目标:
要了解哪些代码将被编译到特定目标平台,您可以将目标平台视为附加到 Kotlin 源文件的标签。Kotlin 使用这些标签来确定如何编译您的代码、生成哪些二进制文件,以及代码中允许使用哪些语言结构和依赖关系。
如果你的项目只有一个目标平台(例如 JVM),你可以从公共代码中访问特定于该平台的符号,并且这些符号具有相应的可见性。但是,一旦你添加了第二个目标平台,这些特定于该平台的符号在公共代码中将无法使用。在项目迁移和其他中间阶段,请务必注意此限制。
如果您也想将greeting.kt文件编译到.js其他位置,只需声明 JS 目标即可。commonMain然后,代码会收到一个与 JS 目标对应的额外js标签,该标签指示 Kotlin 生成.js文件:
这就是 Kotlin 编译器处理通用代码的方式,它将代码编译成所有已声明的目标平台。请参阅“源集”了解如何编写特定于平台的代码。
源集
Kotlin源文件集是一组包含自身目标、依赖项和编译器选项的源文件。它是跨平台项目中共享代码的主要方式。
多平台项目中的每个源集:
拥有一个对特定项目而言独一无二的名称。
包含一组源文件和资源,通常存储在与源集同名的目录中。
指定此源代码集中的代码将编译到的目标数据集。这些目标数据集会影响此源代码集中可用的语言结构和依赖项。
它定义了自身的依赖项和编译器选项。
Kotlin 提供了一系列预定义的源集。其中之一是 <source_set> commonMain,它存在于所有多平台项目中,并编译成所有已声明的目标平台。
在 Kotlin 多平台项目中,您可以像操作目录一样与源集进行交互src。例如,一个包含 <source_set> commonMain、 <source_set> iosMain 和jvmMai<source_set> 源集的项目具有以下结构:

在 Gradle 脚本中,您可以在kotlin.sourceSets {}代码块内按名称访问源集:
kotlin {
// Targets declaration:
// …
// Source set declaration:
sourceSets {
commonMain {
// Configure the commonMain source set
}
}
}除此之外commonMain,其他源集可以是特定于平台的,也可以是中间的。
平台特定的源集
虽然只使用通用代码很方便,但这并非总是可行。代码会commonMain编译到所有已声明的目标平台,而 Kotlin 不允许你在那里使用任何平台特定的 API。
在一个包含原生应用和JS目标的多平台项目中,以下代码commonMain无法编译:
// commonMain/kotlin/common.kt
// Doesn't compile in common code
fun greeting() {
java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}为了解决这个问题,Kotlin 创建了平台特定的源集,也称为平台源集。每个目标都有一个对应的平台源集,该源集仅编译到该目标平台。例如,一个jvm目标有一个对应的jvmMain源集,该源集仅编译到 JVM 平台。Kotlin 允许在这些源集中使用平台特定的依赖项,例如,在以下代码中使用 JDK jvmMain:
// jvmMain/kotlin/jvm.kt
// You can use Java dependencies in the `jvmMain` source set
fun jvmGreeting() {
java.io.File("greeting.txt").writeText("Hello, Multiplatform!")
}编译为特定目标
针对特定目标进行编译时,Kotlin 会处理多个源文件集。当 Kotlin 将一个多平台项目编译为特定目标时,它会收集所有标记为该目标的源文件集,并从中生成二进制文件。
考虑一个包<target> jvm、iosArm6<target> 和j<target> 目标的示例。Kotlin 会commonMain为通用代码创建源集,并为特定目标创建相应的 <target> jvmMain、iosArm64Mai<target> 和jsMai<target> 源集:
在编译到 JVM 时,Kotlin 会选择所有标记为“JVM”的源集,即 <source_set>jvmMain和 <source_set> commonMain。然后,它将它们一起编译成 JVM 类文件:
因为 Kotlin 会编译commonMain并jvmMain一起编译,所以生成的二进制文件包含来自 和commonMain的声明jvmMain。
在处理多平台项目时,请记住:
如果您希望 Kotlin 将代码编译到特定平台,请声明相应的目标。
要选择用于存储代码的目录或源文件,首先要决定要与哪些目标共享代码:
如果代码在所有目标之间共享,则应在
commonMain.如果代码仅用于一个目标平台,则应在该目标平台的特定源代码集中定义该代码(例如,
jvmMainJVM 平台)。
用特定平台源代码集编写的代码可以访问来自通用源代码集的声明。例如
<path>中的代码jvmMain可以使用<path>中的代码commonMain。但是,反过来却不行:commonMain不能使用<path>中的代码jvmMain。使用特定平台源代码集编写的代码可以使用相应的平台依赖项。例如,代码
jvmMain可以使用仅限 Java 使用的库,例如Guava或Spring。
中间源集
简单的多平台项目通常只包含通用代码和平台特定代码。commonMain通用源代码集代表所有已声明目标共享的通用代码。平台特定源代码集(例如 jvmMain<target>)代表仅编译到相应目标平台的特定代码。
在实际应用中,你通常需要更细粒度的代码共享。
假设你需要面向所有新款苹果设备和安卓设备:
kotlin {
android()
iosArm64() // 64-bit iPhone devices
macosArm64() // Modern Apple Silicon-based Macs
watchosArm64() // Modern 64-bit Apple Watch devices
tvosArm64() // Modern Apple TV devices
}您需要一个源集来添加一个为所有苹果设备生成 UUID 的函数:
import platform.Foundation.NSUUID
fun randomUuidString(): String {
// You want to access Apple-specific APIs
return NSUUID().UUIDString()
}您无法将此函数添加到commonMain.中。commonMain会被编译到所有已声明的目标平台,包括 Android,但它是 Apple 特有的 API,在 Android 上不可用。如果您尝试在 中platform.Foundation.NSUUID引用 ,Kotlin 会显示错误。NSUUIDcommonMain
您可以将此代码复制粘贴到每个 Apple 特定的源代码集中:iosArm64Main、watchosArm64Main和tvosArm64Main。但不建议这样做macosArm64Main,因为这样复制代码容易出错。
为了解决这个问题,您可以使用中间源集。中间源集是一种 Kotlin 源集,它可以编译成项目中的部分目标,但并非全部。中间源集有时也被称为分层源集或简称为分层结构。
Kotlin 默认会创建一些中间源集。在本例中,最终的项目结构如下所示:
图中底部的彩色方块代表特定平台的源集。为清晰起见,省略了目标标签。
该appleMain代码块是 Kotlin 创建的中间源代码集,用于共享编译为 Apple 特定目标平台的代码。该appleMain源代码集仅编译为 Apple 目标平台。因此,Kotlin 允许在其中使用 Apple 特定的 API appleMain,您可以在此处添加randomUUID()函数。
请参阅分层项目结构,查找 Kotlin 默认创建和设置的所有中间源集,并了解如果 Kotlin 默认情况下没有提供您需要的中间源集,您应该怎么做。
在编译到特定目标平台时,Kotlin 会获取所有带有该目标标签的源文件集,包括中间源文件集。因此,在编译到平台目标平台时,所有在 <source_name> commonMain、appleMai<intermediate_source_name> <source_name> 源文件中编写的代码iosArm64Main都会被合并。iosArm64
如果某些源代码集没有源代码,这也没关系。例如,在 iOS 开发中,通常不需要提供专门针对 iOS 设备但不适用于 iOS 模拟器的代码,
iosArm64Main因此这种情况很少见。
苹果设备和模拟器目标
与测试集成
实际项目中,除了主生产代码之外,还需要测试。因此,默认情况下创建的所有源代码集都带有 <test_name>Main和 Test<test_name> 后缀 <test_name> Main 包含生产代码,而 <test_name>Test包含针对该代码的测试。它们之间的连接会自动建立,测试Main无需额外配置即可使用代码提供的 API。
这些Test对应的测试源集也类似于 test Main。例如,commonTestest 是 test 的对应测试源集commonMain,它可以编译成所有声明的目标,从而允许您编写通用测试。平台特定的测试源集,例如 jvmTesttest,用于编写平台特定的测试,例如 JVM 特定的测试或需要 JVM API 的测试。
除了需要一个源代码集来编写通用测试之外,你还需要一个多平台测试框架。Kotlin 提供了一个默认kotlin.test库,其中包含@kotlin.Test注解和各种断言方法,例如 @TestassertEquals和 @Test assertTrue。
您可以像编写常规测试一样,为每个平台及其各自的源代码集编写平台特定的测试。与主代码一样,您可以为每个源代码集添加平台特定的依赖项,例如JUnitJVM 和XCTestiOS。要针对特定目标运行测试,请使用<targetName>Test相应的任务。