分层项目结构

Multiplatform projects support hierarchical structures. This means you can arrange a hierarchy of intermediate source sets for sharing the common code among some, but not all, supported targets. Using intermediate source sets has some important advantages:

  • If you’re a library author, and you want to provide a specialized API, you can use an intermediate source set for some, but not all, targets – for example, an intermediate source set for Kotlin/Native targets but not for Kotlin/JVM ones.
  • If you want to use platform-dependent libraries in your project, you can use an intermediate source set to use that specific API in several native targets. For example, you can have access to iOS-specific dependencies, such as Foundation, when sharing code across all iOS targets.
  • Some libraries aren’t available for particular platforms. Specifically, native libraries are only available for source sets that compile to Kotlin/Native. Using an intermediate source set will solve this issue.

The Kotlin toolchain ensures that each source set has access only to the API that is available for all targets to which that source set compiles. This prevents cases like using a Windows-specific API and then compiling it to macOS, resulting in linkage errors or undefined behavior at runtime.

There are 3 ways to create a target hierarchy:

Default hierarchy

The default target hierarchy is Experimental. It may be changed in future Kotlin releases without prior notice. For Kotlin Gradle build scripts, opting in is required with @OptIn(ExperimentalKotlinGradlePluginApi::class).

分层项目结构 - 图1

Starting with Kotlin 1.8.20, you can set up a source set hierarchy in your multiplatform projects with the default target hierarchy. It’s a template for all possible targets and their shared source sets hardcoded in the Kotlin Gradle plugin.

搭建项目

To set up a hierarchy, call targetHierarchy.default() in the kotlin block of your build.gradle(.kts) file and list all of the targets you need. For example:

【Kotlin】

  1. @OptIn(ExperimentalKotlinGradlePluginApi::class)
  2. kotlin {
  3. // Enable the default target hierarchy:
  4. targetHierarchy.default()
  5. android()
  6. iosArm64()
  7. iosSimulatorArm64()
  8. }

【Groovy】

  1. kotlin {
  2. // Enable the default target hierarchy:
  3. targetHierarchy.default {
  4. }
  5. android()
  6. iosArm64()
  7. iosSimulatorArm64()
  8. }

When you declare the final targets android, iosArm64, and iosSimulatorArm64 in your code, the Kotlin Gradle plugin finds suitable shared source sets from the template and creates them for you. The resulting hierarchy looks like this:

An example of using the default target hierarchy{thumbnail=”true” width=”350” thumbnail-same-file=”true”}

Green source sets are actually created and present in the project, while gray ones from the default template are ignored. The Kotlin Gradle plugin hasn’t created the watchos source set, for example, because there are no watchOS targets in the project.

If you add a watchOS target, like watchosArm64, the watchos source set is created, and the code from the apple, native, and common source sets is compiled to watchosArm64 as well.

In this example, the apple and native source sets compile only to the iosArm64 and iosSimulatorArm64 targets. Despite their names, they have access to the full iOS API. This can be counter-intuitive for source sets like native, as you might expect that only APIs available on all native targets are accessible in this source set. This behavior may change in the future.

分层项目结构 - 图3

Adjust the resulting hierarchy

You can further configure the resulting hierarchy manually using the dependsOn relation. To do so, apply the by getting construction for the source sets created with targetHierarchy.default().

Consider this example of a project with a source set shared between the jvm and native targets only:

【Kotlin】

  1. @OptIn(ExperimentalKotlinGradlePluginApi::class)
  2. kotlin {
  3. // Enable the default target hierarchy:
  4. targetHierarchy.default()
  5. jvm()
  6. iosArm64()
  7. // the rest of the necessary targets...
  8. sourceSets {
  9. val commonMain by getting
  10. val jvmAndNativeMain by creating {
  11. dependsOn(commonMain)
  12. }
  13. val nativeMain by getting {
  14. dependsOn(jvmAndNativeMain)
  15. }
  16. val jvmMain by getting {
  17. dependsOn(jvmAndNativeMain)
  18. }
  19. }
  20. }

【Groovy】

  1. kotlin {
  2. // Enable the default target hierarchy:
  3. targetHierarchy.default {
  4. }
  5. jvm()
  6. iosArm64()
  7. // The rest of the other targets, if needed.
  8. sourceSets {
  9. commonMain {
  10. }
  11. jvmAndNativeMain {
  12. dependsOn(commonMain)
  13. }
  14. nativeMain {
  15. dependsOn(jvmAndNativeMain)
  16. }
  17. jvmMain {
  18. dependsOn(jvmAndNativeMain)
  19. }
  20. }
  21. }

It can be cumbersome to remove dependsOn relations that are automatically created by the targetHierarchy.default() call. In that case, use an entirely manual configuration instead of calling the default hierarchy.

We’re currently working on an API to create your own target hierarchies. It will be useful for projects whose hierarchy configurations are significantly different from the default template.

This API is not ready yet, but if you’re eager to try it, look into the targetHierarchy.custom { ... } block and the declaration of targetHierarchy.default() as an example. Keep in mind that this API is still in development. It might not be tested, and can change in further releases.

分层项目结构 - 图4

See the full hierarchy template

When you declare the targets to which your project compiles, the plugin picks the shared source sets based on the specified targets from the template and creates them in your project.

Default target hierarchy

This example only shows the production part of the project, omitting the Main suffix (for example, using common instead of commonMain). However, everything is the same for *Test sources as well.

分层项目结构 - 图6

Target shortcuts

The Kotlin Multiplatform plugin provides some predefined target shortcuts for creating structures for common target combinations:

Target shortcutTargets
iosiosArm64, iosX64
watchoswatchosArm32, watchosArm64, watchosX64
tvostvosArm64, tvosX64

All shortcuts create similar hierarchical structures in the code. For example, you can use theios() shortcut to create a multiplatform project with 2 iOS-related targets, iosArm64 and iosX64, and a shared source set:

【Kotlin】

  1. kotlin {
  2. ios() // iOS device and the iosX64 simulator target; iosMain and iosTest source sets
  3. }

【Groovy】

  1. kotlin {
  2. ios() // iOS device and the iosX64 simulator target; iosMain and iosTest source sets
  3. }

In this case, the hierarchical structure includes the intermediate source sets iosMain and iosTest, which are used by the platform-specific source sets:

Code shared for iOS targets

The resulting hierarchical structure will be equivalent to the code below:

【Kotlin】

  1. kotlin {
  2. iosX64()
  3. iosArm64()
  4. sourceSets {
  5. val commonMain by getting
  6. val iosX64Main by getting
  7. val iosArm64Main by getting
  8. val iosMain by creating {
  9. dependsOn(commonMain)
  10. iosX64Main.dependsOn(this)
  11. iosArm64Main.dependsOn(this)
  12. }
  13. }
  14. }

【Groovy】

  1. kotlin {
  2. iosX64()
  3. iosArm64()
  4. sourceSets {
  5. iosMain {
  6. dependsOn(commonMain)
  7. iosX64Main.dependsOn(it)
  8. iosArm64Main.dependsOn(it)
  9. }
  10. }
  11. }

Target shortcuts and ARM64 (Apple Silicon) simulators

The ios, watchos, and tvos target shortcuts don’t include the simulator targets for ARM64 (Apple Silicon) platforms: iosSimulatorArm64, watchosSimulatorArm64, and tvosSimulatorArm64. If you use the target shortcuts and want to build the project for an Apple Silicon simulator, make the following adjustment to the build script:

  1. Add the *SimulatorArm64 simulator target you need.
  2. Connect the simulator target with the shortcut using the dependsOn relation between source sets.

【Kotlin】

  1. kotlin {
  2. ios()
  3. // Add the ARM64 simulator target
  4. iosSimulatorArm64()
  5. val iosMain by sourceSets.getting
  6. val iosTest by sourceSets.getting
  7. val iosSimulatorArm64Main by sourceSets.getting
  8. val iosSimulatorArm64Test by sourceSets.getting
  9. // Set up dependencies between the source sets
  10. iosSimulatorArm64Main.dependsOn(iosMain)
  11. iosSimulatorArm64Test.dependsOn(iosTest)
  12. }

【Groovy】

  1. kotlin {
  2. ios()
  3. // Add the ARM64 simulator target
  4. iosSimulatorArm64()
  5. // Set up dependencies between the source sets
  6. sourceSets {
  7. // ...
  8. iosSimulatorArm64Main {
  9. dependsOn(iosMain)
  10. }
  11. iosSimulatorArm64Test {
  12. dependsOn(iosTest)
  13. }
  14. }
  15. }

Manual configuration

You can manually introduce an intermediate source in the source set structure. It will hold the shared code for several targets.

For example, here’s what to do if you want to share code among native Linux, Windows, and macOS targets (linuxX64, mingwX64, and macosX64):

Manually configured hierarchical structure

  1. Add the intermediate source set desktopMain, which holds the shared logic for these targets.
  2. Specify the source set hierarchy using the dependsOn relation.

The resulting hierarchical structure will look like this:

【Kotlin】

  1. kotlin {
  2. linuxX64()
  3. mingwX64()
  4. macosX64()
  5. sourceSets {
  6. val desktopMain by creating {
  7. dependsOn(commonMain.get())
  8. }
  9. val linuxX64Main by getting {
  10. dependsOn(desktopMain)
  11. }
  12. val mingwX64Main by getting {
  13. dependsOn(desktopMain)
  14. }
  15. val macosX64Main by getting {
  16. dependsOn(desktopMain)
  17. }
  18. }
  19. }

【Groovy】

  1. kotlin {
  2. linuxX64()
  3. mingwX64()
  4. macosX64()
  5. sourceSets {
  6. desktopMain {
  7. dependsOn(commonMain.get())
  8. }
  9. linuxX64Main {
  10. dependsOn(desktopMain)
  11. }
  12. mingwX64Main {
  13. dependsOn(desktopMain)
  14. }
  15. macosX64Main {
  16. dependsOn(desktopMain)
  17. }
  18. }
  19. }

You can have a shared source set for the following combinations of targets:

  • JVM or Android + JS + Native
  • JVM or Android + Native
  • JS + Native
  • JVM or Android + JS
  • Native

Kotlin doesn’t currently support sharing a source set for these combinations:

  • Several JVM targets
  • JVM + Android targets
  • Several JS targets

If you need to access platform-specific APIs from a shared native source set, IntelliJ IDEA will help you detect common declarations that you can use in the shared native code. For other cases, use the Kotlin mechanism of expected and actual declarations.