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 don't have to share it across all, supported targets, (Target (Gradle Kotlin-MP))

Clarification on "not all"

The phrase "not all" in "sharing the common code among some, but not all, supported targets" means that when setting up a hierarchical source set structure in Kotlin Multiplatform projects, you don't necessarily need to share the common code across every single platform (or target) you're building for. Instead, you can choose to share the code among a subset of the supported targets.

What is 'intermediate source sets'?

Intermediate source sets in Kotlin Multiplatform projects are shared source sets that are used to share code between a subset of targets rather than all of them. These source sets act as a middle layer between the common code (shared across all targets) and platform-specific code, allowing for reuse across some, but not all, of the supported platforms.

Key points about intermediate source sets:

  1. Sharing Code Across Some Targets: Intermediate source sets are designed to hold code that is shared between specific platforms. For example, you might have an iosMain source set that is shared between all iOS targets but not other platforms like Android or JVM.

  2. Customization for Specific Platforms: You can include platform-specific APIs or logic that apply to a subset of platforms (e.g., native platforms like iOS, macOS) while avoiding this code for others (like JVM or JS).

  3. Layer Between Common and Platform-Specific Code:

    • The common source set is shared across all targets.
    • Intermediate source sets are placed between the common source set and specific platform source sets.
    • Platform-specific source sets are at the bottom layer and contain code unique to that platform.

Example:

In a Kotlin Multiplatform project, you may define an intermediate source set for native targets (iOS and macOS) but exclude JVM and JS from sharing this code.

kotlin {
    jvm()
    ios()
    macosX64()

    sourceSets {
        val commonMain by getting
        val nativeMain by creating {  // Intermediate source set shared between iOS and macOS
            dependsOn(commonMain)
        }
        iosMain.get().dependsOn(nativeMain)
        macosX64Main.get().dependsOn(nativeMain)
    }
}

In this example:

  • commonMain is shared across all targets.
  • nativeMain is an intermediate source set shared by both iosMain and macosX64Main, but not by jvmMain.
  • Code placed in nativeMain can be used by both iOS and macOS, but not by JVM.

Why Use Intermediate Source Sets?

  1. Platform-Specific APIs: You can use APIs specific to a certain subset of platforms (like native iOS or macOS APIs) without polluting the code that gets compiled for other platforms like JVM or JS.
  2. Code Reuse: It reduces duplication by sharing common code between certain platforms.
  3. Flexibility: It gives you more granular control over code-sharing while respecting platform-specific constraints and APIs.

Example Structure:

In a project with ios and android targets, you might have these source sets:

  • commonMain (shared across all platforms)
  • iosMain (specific to iOS)
  • androidMain (specific to Android)
  • appleMain (intermediate source set, shared by iOS and macOS 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

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()
}

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

Colored 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 provides both type-safe and static accessors for all of the source sets from the default hierarchy template, so you can reference them without by getting or by creating constructs compared to the manual configuration.

If you try to access the source set without declaring the corresponding target first, you'll see a warning:

kotlin {
    androidTarget()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        iosMain.dependencies {
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:%coroutinesVersion%")
        }
        // Warning: accessing source set without declaring the target
        linuxX64Main { }
    }
}
kotlin {
    androidTarget()
    iosArm64()
    iosSimulatorArm64()

    sourceSets {
        iosMain {
            dependencies {
                implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:%coroutinesVersion%'
            }
        }
        // Warning: accessing source set without declaring the target
        linuxX64Main { }
    }
}

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.

{style="note"}

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.

We're currently working on an API to create your own hierarchy templates. This will be useful for projects whose hierarchy configurations differ significantly from the default template.

This API is not ready yet, but if you're eager to try it, look into the applyHierarchyTemplate {} block and the declaration of KotlinHierarchyTemplate.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.

{style="tip"}

See the full hierarchy template {initial-collapse-state="collapsed" collapsible="true"}

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

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.

{style="tip"}

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.


Backlinks