本文阐述了 Kotlin 多平台项目结构的高级概念,以及它们如何映射到 Gradle 实现。如果您需要使用 Gradle 构建的底层抽象(配置、任务、发布等),或者正在为 Kotlin 多平台构建创建 Gradle 插件,这些信息将非常有用。
如果您符合以下情况,此页面可能对您有所帮助:
需要在多个目标平台之间共享代码,但 Kotlin 不会为这些平台创建源代码集。
想要为 Kotlin 多平台构建创建 Gradle 插件,或者需要处理 Gradle 构建的底层抽象,例如配置、任务、发布等。
在多平台项目中,理解依赖管理的关键一点是了解 Gradle 式项目或库依赖关系与dependsOnKotlin 特有的源集关系之间的区别:
dependsOn是通用源集和平台特定源集之间的关系,它支持源集层次结构,并允许在多平台项目中共享代码。对于默认源集,层次结构会自动管理,但在特定情况下可能需要进行修改。库和项目依赖项通常照常运行,但要在多平台项目中正确管理它们,您应该了解Gradle 依赖项如何解析为细粒度的源集 →用于编译的源集依赖项。
在深入学习高级概念之前,我们建议先学习多平台项目结构的基础知识。
dependsOn 和源集层次结构
通常情况下,你会处理的是依赖关系,而不是它们之间的dependsOn关系。然而,深入研究dependsOn依赖关系对于理解 Kotlin 多平台项目的底层工作原理至关重要。
dependsOn这是 Kotlin 特有的两个 Kotlin 源集之间的关系。例如,当一个源集jvmMain依赖于一个平台特定的源集,而该源集commonMain又iosArm64Main依赖于另一个平台特定的源集时,这种关系就成立iosMain。
考虑一个使用 Kotlin 源集的通用示例A。B该表达式A.dependsOn(B)指示 Kotlin:
A观察 APIB,包括内部声明。A可以为预期声明提供实际实现B。这是一个必要且充分条件,因为当且仅当直接或间接地提供时A,才能提供。actualsBA.dependsOn(B)BA除了自身的目标之外,还应该编译成所有其他目标。A继承了所有常规依赖项B。
这种dependsOn关系创建了一个树状结构,称为源集层次结构。以下是一个典型的移动开发项目示例android,其中包含iosArm64(iPhone 设备)和iosSimulatorArm64(适用于 Apple Silicon Mac 的 iPhone 模拟器):
箭头表示dependsOn关系。这些关系在平台二进制文件的编译过程中会被保留。Kotlin 就是这样理解应该iosMain从某个来源访问 API commonMain,而不是从另一个来源访问iosArm64Main:
dependsOn关系是通过KotlinSourceSet.dependsOn(KotlinSourceSet)调用来配置的,例如:
kotlin {
// Targets declaration
sourceSets {
// Example of configuring the dependsOn relation
iosArm64Main.dependsOn(commonMain)
}
}此示例展示了如何
dependsOn在构建脚本中定义关系。不过,Kotlin Gradle 插件默认会创建源集并设置这些关系,因此您无需手动操作。dependsOn在构建脚本中,关联关系与代码块是分开声明的dependencies {}。这是因为关联关系dependsOn并非常规依赖项;相反,它是 Kotlin 源代码集之间的一种特定关联,用于在不同目标平台之间共享代码。
你不能使用dependsOimport 来声明对已发布库或其他 Gradle 项目的常规依赖项。例如,你不能设置 importcommonMain来依赖于库commonMain的kotlinx-coroutines-cor<library> 或调用 import commonTest.dependsOn(commonMain)。
声明自定义源集
在某些情况下,您可能需要在项目中使用自定义的中间源文件集。例如,假设一个项目编译后会同时运行在 JVM、JS 和 Linux 平台上,而您只想在 JVM 和 JS 之间共享某些源文件。在这种情况下,您应该为这两个目标平台找到一个特定的源文件集,具体内容请参见“多平台项目结构基础”一文。
Kotlin 不会自动创建这样的源集。这意味着您需要使用以下by creating构造函数手动创建它:
kotlin {
jvm()
js()
linuxX64()
sourceSets {
// Create a source set named "jvmAndJs"
val jvmAndJsMain by creating {
// …
}
}
}然而,Kotlin 仍然不知道如何处理或编译这个源集。如果画一个图,这个源集会是孤立的,没有任何目标标签:
为了解决这个问题,可以通过添加多个关系将其纳入jvmAndJsMain层次结构中:dependsOn
kotlin {
jvm()
js()
linuxX64()
sourceSets {
val jvmAndJsMain by creating {
// Don't forget to add dependsOn to commonMain
dependsOn(commonMain.get())
}
jvmMain {
dependsOn(jvmAndJsMain)
}
jsMain {
dependsOn(jvmAndJsMain)
}
}
}这里,jvmMain.dependsOn(jvmAndJsMain)将 JVM 目标添加到jvmAndJsMain,并将jsMain.dependsOn(jvmAndJsMain)JS 目标添加到jvmAndJsMain。
最终项目结构如下所示:
手动配置
dependsOn关系会禁用默认层级模板的自动应用。请参阅“其他配置”了解更多相关信息以及处理方法。
对其他库或项目的依赖
在多平台项目中,您可以对已发布的库或另一个 Gradle 项目设置常规依赖项。
Kotlin 多平台通常以典型的 Gradle 方式声明依赖项。与 Gradle 类似,您需要:
dependencies {}在构建脚本中使用该代码块。为依赖项选择合适的范围,例如,
implementation或api。引用依赖项的方式有两种:如果依赖项发布在仓库中,则指定其坐标,例如
"com.google.guava:guava:32.1.2-jre"<repository>;如果依赖项是同一构建中的 Gradle 项目,则指定其路径,例如<gradle project>project(":utils:concurrency")。
多平台项目中的依赖配置有一些特殊之处。每个 Kotlin 源集都有自己的依赖dependencies {}项配置块。这允许您在特定于平台的源集中声明特定于平台的依赖项:
kotlin {
// Targets declaration
sourceSets {
jvmMain.dependencies {
// This is jvmMain's dependencies, so it's OK to add a JVM-specific dependency
implementation("com.google.guava:guava:32.1.2-jre")
}
}
}通用依赖项的处理比较棘手。例如,考虑一个声明依赖于一个多平台库的多平台项目kotlinx.coroutines:
kotlin {
android() // Android
iosArm64() // iPhone devices
iosSimulatorArm64() // iPhone simulator on Apple Silicon Mac
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3")
}
}
}依赖关系解析中有三个重要概念:
多平台依赖项会向下传递到整个
dependsOn结构中。当您向<source_name>添加依赖项时commonMain,它将自动添加到所有dependsOn直接或间接在<source_name>中声明关系的源集中commonMain。在这种情况下,依赖项确实已自动添加到所有
*Main源集:iosMain、、和。所有这些源集都继承了jvmMain源集的依赖项,因此您无需手动将其复制粘贴到所有源集中:iosSimulatorArm64MainiosArm64Mainkotlin-coroutines-corecommonMain传播机制允许您通过选择特定的源集来指定要接收已声明依赖项的作用域。例如,如果您想
kotlinx.coroutines在 iOS 上使用某个依赖项,但不要在 Android 上使用,则可以iosMain仅将此依赖项添加到源集。如上所述,源集→多平台库依赖关系代表依赖关系解析的中间状态。最终的解析状态始终由源集→源集依赖关系表示。
commonMainorg.jetbrians.kotlinx:kotlinx-coroutines-core:1.7.3最终源集→源集依赖关系并非
dependsOn关系。为了推断细粒度的源集依赖关系,Kotlin 会读取每个多平台库随附发布的源集结构。完成此步骤后,每个库在内部将不再作为一个整体来表示,而是作为其源集的集合。请参阅以下示例
kotlinx-coroutines-core:Kotlin 会获取每个依赖关系,并将其解析为依赖项的源集集合。该集合中的每个依赖源集都必须具有兼容的目标。如果一个依赖源集至少编译成与使用者源集相同的目标,则称该依赖源集具有兼容的目标。
考虑一个例子,
commonMain其中示例项目编译后得到android、iosArm64和iosSimulatorArm64:首先,它解决了对 的依赖
kotlinx-coroutines-core.commonMain。这是因为kotlinx-coroutines-core会编译成所有可能的 Kotlin 目标。因此,它的commonMain会编译成所有可能的目标,包括所需的android、iosArm64和iosSimulatorArm64。其次,
commonMain这取决于kotlinx-coroutines-core.concurrentMain。由于concurrentMain它kotlinx-coroutines-core编译成除 JS 之外的所有目标,因此它与消费者项目的目标相匹配commonMain。
然而,像
iosArm64Main协程这样的源集与消费者的源集不兼容commonMain。即使iosArm64Main编译成的目标之一commonMain,即,iosArm64它也不会编译成android或iosSimulatorArm64。依赖关系解析的结果直接影响代码中哪些部分
kotlinx-coroutines-core可见:
使不同源集中的通用依赖项版本保持一致
在 Kotlin 多平台项目中,公共源代码集会被多次编译以生成 klib,并且每次编译都会进行一次配置。为了生成一致的二进制文件,每次编译时都应该使用相同版本的多平台依赖项。Kotlin Gradle 插件有助于统一这些依赖项,确保每个源代码集的有效依赖项版本相同。
在上面的例子中,假设你想androidx.navigation:navigation-compose:2.7.7向androidMain源集添加依赖项。你的项目已经明确声明了源集kotlinx-coroutines-core:1.7.3的依赖项commonMain,但是版本为 2.7.7 的 Compose Navigation 库需要 Kotlin 协程 1.8.0 或更高版本。
由于KotlincommonMain和 iOSandroidMain协程库是一起编译的,Kotlin Gradle 插件会在两个协程库版本之间进行选择,并将其应用于kotlinx-coroutines-core:1.8.0源commonMain集。但为了确保通用代码在所有已配置的目标平台上编译一致,iOS 源集也需要使用相同的依赖版本。因此,Gradle 也会将kotlinx.coroutines-*:1.8.0依赖关系传播到iosMain源集。
*Main依赖项在不同的源集*Test之间是分别进行的。源集的 Gradle 配置*Test包含源集的所有依赖项*Main,反之则不然。因此,您可以使用较新的库版本测试项目,而不会影响主代码。
例如,您的*Main源集中存在 Kotlin 协程 1.7.3 依赖项,该依赖项会传播到项目中的每个源集。但是,iosTest您决定在某个源集中将版本升级到 1.8.0 以测试新发布的库。根据相同的算法,此依赖项将在整个*Test源集树中传播,因此每个*Test源集都将使用该kotlinx.coroutines-*:1.8.0依赖项进行编译。
汇编
与单平台项目不同,Kotlin 多平台项目需要多次启动编译器才能构建所有工件。每次启动编译器都会进行一次Kotlin 编译。
例如,以下是前面提到的 Kotlin 编译过程中生成 iPhone 设备二进制文件的方式:
Kotlin 编译按目标分组。默认情况下,Kotlin 会为每个目标创建两个编译版本:一个main用于生产环境源代码,另一个test用于测试环境源代码。
在构建脚本中访问编译文件的方式类似。首先选择一个 Kotlin 目标,然后访问compilations其中的容器,最后通过名称选择所需的编译文件:
kotlin {
// Declare and configure the JVM target
jvm {
val mainCompilation: KotlinJvmCompilation = compilations.getByName("main")
}
}