skip to content

When does the default hierarchy template fall short, and how do you create a custom intermediate source set (e.g. a shared `jvmAndAndroidMain`) while keeping the right expect/actual visibility?

level: seniorimportance: nice to knowfreq 30%

answer

  1. Template lacks cross-family groups (JVM+Android)
  2. sourceSets { creating { dependsOn(commonMain) } }
  3. Children dependsOn the custom set
  4. Custom set sees API intersection of its children
  5. Modern: applyDefaultHierarchyTemplate { group { } }

basics

~20 s

The default groups only cover common families like Apple or native. When you want to share code between unrelated targets (like JVM and Android), you create your own intermediate source set and link it with dependsOn.

solid answer

~40 s

The default template only provides groupings it knows about (`nativeMain`, `appleMain`, `iosMain`, etc.). It does NOT create cross-family groups such as JVM+Android, or 'all targets except JS'. For those you declare a **custom intermediate source set** in the `sourceSets {}` block and wire it manually with `dependsOn`. Example: create `jvmAndAndroidMain`, make it `dependsOn(commonMain)`, then make both `jvmMain` and `androidMain` `dependsOn(jvmAndAndroidMain)`. Now JVM/Android-shared code (e.g. `java.time.*` usage) lives once, and an `expect` in commonMain can have its `actual` placed in `jvmAndAndroidMain`. You can combine this with the default template — they coexist; you just add edges the template didn't. Keep the rule: each leaf must still reach exactly one `actual` per `expect`, and the custom set sees only the union of APIs common to its children.

code

kotlin · 10 lines
kotlin
kotlin {
    applyDefaultHierarchyTemplate {
        group("jvmAndAndroid") {
            withJvm()
            withAndroidTarget()
        }
    }
    jvm(); androidTarget(); iosArm64()
    // Now jvmAndAndroidMain/Test exist; jvmMain & androidMain dependsOn it.
}

go deeper

for a junior

Knows you can add your own shared source set when needed.

for a middle

Can create an intermediate set with dependsOn and link children to it.

for a senior

Knows the template's limits, uses the group {} DSL, and respects API-intersection and single-actual rules.

for a principal

Designs custom grouping strategy across a large target matrix, balances template extension vs. manual wiring, and avoids orphan-source-set/maintenance pitfalls.

## When the template falls short The default hierarchy template encodes the *standard* target families: native, apple, ios, watchos, tvos, linux, mingw, plus the test mirrors. It deliberately does NOT create groupings like: - **JVM + Android** (both run on a JVM and can share `java.*` APIs, but they aren't one family in the template). - **All targets except JS/Wasm**. - Any project-specific grouping (e.g. 'mobile' = android + ios). ## Creating a custom intermediate source set You add it in `sourceSets {}` and wire edges yourself. It coexists with the template's auto-wiring. ```kotlin kotlin { jvm() androidTarget() iosArm64(); iosSimulatorArm64() applyDefaultHierarchyTemplate() // still wires appleMain/iosMain/nativeMain sourceSets { val commonMain by getting val jvmAndAndroidMain by creating { dependsOn(commonMain) } val jvmMain by getting { dependsOn(jvmAndAndroidMain) } val androidMain by getting { dependsOn(jvmAndAndroidMain) } // iosMain/appleMain remain handled by the template } } ``` Now `jvmAndAndroidMain` can use `java.time.LocalDate`, and an `expect fun formatDate(...)` in `commonMain` can have a single `actual` in `jvmAndAndroidMain` shared by both JVM and Android leaves. ## Visibility rules to respect - A custom intermediate set sees the **intersection** of APIs available to all its child targets. `jvmAndAndroidMain` can use `java.*` (both are JVM-based) but not iOS APIs. - The compiler still enforces **exactly one reachable `actual`** per `expect` per leaf. If you put an `actual` in `jvmAndAndroidMain` AND in `jvmMain`, the JVM leaf has two — compile error. - The custom set's `dependsOn(commonMain)` is what unlocks `expect`/`actual` and shared `internal` visibility for it. ## Customizing the template itself Instead of raw `dependsOn`, you can extend the template: ```kotlin applyDefaultHierarchyTemplate { group("jvmAndAndroid") { withJvm() withAndroidTarget() } } ``` This declaratively asks the plugin to generate a `jvmAndAndroidMain`/`Test` group, which is cleaner and less error-prone than manual `dependsOn`. Either approach yields the same graph; the `group {}` DSL is the modern recommendation. ## Pitfall: orphan-source-set warning If you create an intermediate set that some leaf forgets to `dependsOn`, or you mix manual wiring inconsistently with the template, the plugin warns about source sets not reachable by any compilation. Prefer the `group {}` DSL or wire every child explicitly.

  • Why can `jvmAndAndroidMain` use `java.time.LocalDate` but `nativeMain` cannot?
    A source set sees the intersection of its children's APIs; both JVM and Android run on a JVM so java.* is available, whereas native targets have no JVM.
  • What is the cleaner alternative to manual dependsOn wiring for a custom group?
    Use the `applyDefaultHierarchyTemplate { group("...") { withJvm(); withAndroidTarget() } }` DSL so the plugin generates and wires the set for you.

saying these in an interview costs you the question

  • Assuming the template can group any arbitrary targets
  • Forgetting children must dependsOn the custom set
  • Placing duplicate actuals in custom set and a leaf
  • Expecting iOS APIs inside a JVM+Android intermediate set
  • Manually wiring dependsOn AND fighting the template, causing orphan warnings

context