Kotlin Help

Hierarchical project structure

Kotlin Multiplatform projects support hierarchical source set 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 helps you to:

  • Provide a specific API for some targets. For example, a library can add native-specific APIs in an intermediate source set for Kotlin/Native targets but not for Kotlin/JVM ones.

  • Consume a specific API for some targets. For example, you can benefit from a rich API that the Kotlin Multiplatform library provides for some targets that form an intermediate source set.

  • Use platform-dependent libraries in your project. For example, you can access iOS-specific dependencies from the intermediate iOS source set.

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.

The recommended way to set up the source set hierarchy is to use the default hierarchy template. The template covers the most popular cases. If you have a more advanced project, you can configure it manually. This is a more low-level approach: it's more flexible but requires more effort and knowledge.

Default hierarchy template

Starting with Kotlin 1.9.20, the Kotlin Gradle plugin has a built-in default hierarchy template. It contains predefined intermediate source sets for some popular use cases. The plugin sets up those source sets automatically based on the targets specified in your project.

Consider the following example:

kotlin { androidTarget() iosArm64() iosSimulatorArm64() }
kotlin { androidTarget() iosArm64() iosSimulatorArm64() }

When you declare the targets androidTarget, 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 hierarchy template

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.

The Kotlin Gradle plugin creates type-safe accessors for all of the source sets from the default hierarchy template, so you can reference them without by getting or by creating constructions compared to the manual configuration:

kotlin { androidTarget() iosArm64() iosSimulatorArm64() sourceSets { iosMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3") } } }
kotlin { androidTarget() iosArm64() iosSimulatorArm64() sourceSets { iosMain { dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.7.3' } } } }

Additional configuration

You might need to make adjustments to the default hierarchy template. If you have previously introduced intermediate sources manually with dependsOn calls, it cancels the use of the default hierarchy template and leads to this warning:

The Default Kotlin Hierarchy Template was not applied to '<project-name>': Explicit .dependsOn() edges were configured for the following source sets: [<... names of the source sets with manually configured dependsOn-edges...>] Consider removing dependsOn-calls or disabling the default template by adding 'kotlin.mpp.applyDefaultHierarchyTemplate=false' to your gradle.properties Learn more about hierarchy templates: https://kotl.in/hierarchy-template

To solve this issue, configure your project by doing one of the following:

Replacing a manual configuration

Case. All of your intermediate source sets are currently covered by the default hierarchy template.

Solution. Remove all manual dependsOn() calls and source sets with by creating constructions. To check the list of all default source sets, see the full hierarchy template.

Creating additional source sets

Case. You want to add source sets that the default hierarchy template doesn't provide yet, for example, one between a macOS and a JVM target.

Solution:

  1. Reapply the template by explicitly calling applyDefaultHierarchyTemplate().

  2. Configure additional source sets manually using dependsOn():

    kotlin { jvm() macosArm64() iosArm64() iosSimulatorArm64() // Apply the default hierarchy again. It'll create, for example, the iosMain source set: applyDefaultHierarchyTemplate() sourceSets { // Create an additional jvmAndMacos source set: val jvmAndMacos by creating { dependsOn(commonMain.get()) } macosArm64Main.get().dependsOn(jvmAndMacos) jvmMain.get().dependsOn(jvmAndMacos) } }
    kotlin { jvm() macosArm64() iosArm64() iosSimulatorArm64() // Apply the default hierarchy again. It'll create, for example, the iosMain source set: applyDefaultHierarchyTemplate() sourceSets { // Create an additional jvmAndMacos source set: jvmAndMacos { dependsOn(commonMain.get()) } macosArm64Main { dependsOn(jvmAndMacos.get()) } jvmMain { dependsOn(jvmAndMacos.get()) } } }

Modifying source sets

Case. You already have the source sets with the exact same names as those generated by the template, but shared among different sets of targets in your project. For example, a nativeMain source set is shared only among the desktop-specific targets: linuxX64, mingwX64, and macosX64.

Solution. There's currently no way to modify the default dependsOn relations among the template's source sets. It's also important that the implementation and the meaning of source sets, for example, nativeMain, are the same in all projects.

However, you still can do one of the following:

  • Find different source sets for your purposes, either in the default hierarchy template or ones that have been manually created.

  • Opt out of the template completely by adding kotlin.mpp.applyDefaultHierarchyTemplate=false to your gradle.properties file and manually configure all source sets.

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 hierarchy template

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):

  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.

kotlin { linuxX64() mingwX64() macosX64() sourceSets { val desktopMain by creating { dependsOn(commonMain.get()) } linuxX64Main.get().dependsOn(desktopMain) mingwX64Main.get().dependsOn(desktopMain) macosX64Main.get().dependsOn(desktopMain) } }
kotlin { linuxX64() mingwX64() macosX64() sourceSets { desktopMain { dependsOn(commonMain.get()) } linuxX64Main { dependsOn(desktopMain) } mingwX64Main { dependsOn(desktopMain) } macosX64Main { dependsOn(desktopMain) } } }

The resulting hierarchical structure will look like this:

Manually configured hierarchical structure

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.

Last modified: 15 April 2024