skip to content

What is the default hierarchy template in Kotlin Multiplatform, and how does it wire intermediate source sets like `nativeMain` and `appleMain` automatically?

level: middleimportance: must knowfreq 60%

answer

  1. applyDefaultHierarchyTemplate, auto since 1.9.20
  2. nativeMain > appleMain > iosMain chain
  3. Intermediate set only if >=2 child targets
  4. Declaring targets triggers it
  5. actual can sit at any level up the chain

basics

~10 s

When you declare targets, Kotlin auto-creates shared groups like nativeMain and appleMain and links them between common and each platform, so you do not have to wire them by hand.

solid answer

~40 s

The **default hierarchy template** is a built-in source-set graph that the Kotlin Gradle plugin applies automatically (since Kotlin 1.9.20) once you declare your targets. Instead of you manually calling `dependsOn`, declaring e.g. `iosArm64()`, `iosX64()`, `macosArm64()`, `jvm()` makes the plugin create intermediate source sets — `appleMain` (parent of all Apple targets), `nativeMain` (parent of all Kotlin/Native targets), `iosMain` (parent of the iOS variants) — and wire the chain `iosArm64Main → iosMain → appleMain → nativeMain → commonMain`. Each intermediate set only appears if two or more of its children targets exist. You can write platform-shared code (e.g. POSIX or Apple Foundation calls) in `nativeMain`/`appleMain` without duplicating it per leaf. You enable/customize it via `applyDefaultHierarchyTemplate()` or by defining a custom template; the plugin warns if you also wire `dependsOn` manually in a conflicting way.

code

kotlin · 9 lines
kotlin
kotlin {
    applyDefaultHierarchyTemplate() // explicit; usually implicit
    iosArm64(); iosSimulatorArm64(); macosArm64()

    sourceSets {
        // appleMain materializes because >=2 Apple targets exist
        appleMain.dependencies { /* Foundation-shared code lives here */ }
    }
}

go deeper

for a junior

Knows there is an automatic setup so you don't wire every source set by hand.

for a middle

Names applyDefaultHierarchyTemplate, the nativeMain/appleMain/iosMain chain, and that declaring targets triggers it.

for a senior

Explains the >=2-children rule, that actuals can live at any intermediate level, and how compilation gathers the chain per leaf.

for a principal

Decides when to extend vs. replace the template, weighs maintainability of intermediate grouping, and reasons about migration from manual wiring across a large module graph.

## The problem it solves Before the template, you wrote `dependsOn` by hand for every shared group, e.g. creating an `iosMain` and pointing `iosArm64Main`, `iosX64Main`, `iosSimulatorArm64Main` at it, and that at `commonMain`. Tedious and error-prone. ## What the default hierarchy template is Since **Kotlin 1.9.20** the Kotlin Gradle plugin ships a **default hierarchy template** — a predefined map from groups of targets to intermediate source sets. When you declare your targets, the plugin **applies it automatically** (you can also call `applyDefaultHierarchyTemplate()` explicitly). It creates and wires intermediate source sets so each leaf inherits a sensible chain up to `commonMain`. ```kotlin kotlin { // declaring targets is enough to trigger the template jvm() iosArm64() iosSimulatorArm64() macosArm64() // Auto-created/wired (if >=2 children exist): // iosArm64Main -> iosMain // iosSimulatorArm64Main -> iosMain // iosMain -> appleMain // macosArm64Main -> appleMain // appleMain -> nativeMain // nativeMain -> commonMain // jvmMain -> commonMain } ``` ## The standard intermediate groups - **`nativeMain`** — parent of ALL Kotlin/Native targets (iOS, macOS, watchOS, tvOS, linux, mingw). Good for POSIX-level shared code. - **`appleMain`** — parent of all Apple targets (iOS/macOS/watchOS/tvOS). Good for Foundation/Darwin shared code. - **`iosMain`** — parent of the iOS device + simulator variants. - Test mirrors exist too: `nativeTest`, `appleTest`, `iosTest`. ## The "only-if-2+-children" rule An intermediate set materializes **only when at least two of its child targets are declared**. If you declare a single `iosArm64()` and nothing else Apple, the plugin won't bother creating `appleMain` (there is nothing to share). This keeps the graph minimal. ## Why each leaf sees the right expect/actual set Because the chain is transitive, when `iosArm64` is compiled the compiler gathers `commonMain + nativeMain + appleMain + iosMain + iosArm64Main`. An `expect` in `commonMain` can be satisfied by an `actual` placed at ANY level on that chain — e.g. one `actual` in `appleMain` covers all Apple leaves, avoiding duplication. ## Customizing / disabling - `applyDefaultHierarchyTemplate { /* custom */ }` to extend it. - If you set up conflicting manual `dependsOn`, the plugin emits a warning; the recommended path is to rely on the template and only add custom intermediate sets when you need a grouping the template doesn't provide.

  • Why might `appleMain` not exist in a project that only targets `iosArm64`?
    The intermediate set is created only when at least two child targets are present; a single Apple target gives nothing to share, so the plugin skips it.
  • Where can an `actual` for a common `expect` live if it should be shared by all Apple targets?
    In `appleMain` — because every Apple leaf transitively dependsOn it, one `actual` there covers them all.

saying these in an interview costs you the question

  • Thinking you must always wire dependsOn manually
  • Not knowing the >=2-children materialization rule
  • Claiming nativeMain includes the JVM target
  • Believing actuals can only live in leaf source sets
  • Confusing appleMain with iosMain (appleMain is broader)

context