skip to content

You introduce an intermediate source set appleMain shared by iosArm64Main and macosArm64Main. How do per-target dependency declarations and visibility behave for it, and what does dependsOn change?

level: seniorimportance: should knowfreq 30%

answer

  1. appleMain between common and leaves
  2. dependsOn = inheritance + symbol visibility
  3. declare dep at highest resolvable set
  4. default hierarchy template auto-wires
  5. too-high breaks non-matching targets

basics

~10 s

Dependencies you declare on appleMain are inherited by every platform source set that depends on it (iosArm64Main, macosArm64Main). dependsOn wires that inheritance and lets appleMain use Apple-specific APIs shared by those targets.

solid answer

~50 s

An intermediate source set like appleMain sits between commonMain and the leaf platform source sets. You connect it with dependsOn: iosArm64Main.dependsOn(appleMain) and appleMain.dependsOn(commonMain). Dependencies declared in appleMain.dependencies { } then flow to all source sets that dependsOn it, so a library available on all Apple targets is declared once. Visibility also flows: appleMain can use APIs common to its leaf targets (e.g. shared Apple/Darwin platform APIs and any expect declarations it actuals). Modern Kotlin's default hierarchy template often creates appleMain/iosMain automatically when you declare the corresponding targets, so you frequently just use the generated accessor instead of wiring dependsOn manually. The key rule: declare a dependency at the highest source set where all descendant targets can resolve it; declaring it on appleMain avoids repeating it in each Apple leaf and keeps it out of commonMain where non-Apple targets couldn't use it.

code

kotlin · 10 lines
kotlin
kotlin {
    iosArm64(); macosArm64()
    sourceSets {
        val commonMain by getting
        val appleMain by creating { dependsOn(commonMain) }
        val iosArm64Main by getting { dependsOn(appleMain) }
        val macosArm64Main by getting { dependsOn(appleMain) }
        appleMain.dependencies { implementation("io.ktor:ktor-client-darwin:3.0.0") }
    }
}

go deeper

for a junior

Understands that some source sets sit between commonMain and platform leaves.

for a middle

Knows deps declared on an intermediate set are inherited by its descendant targets.

for a senior

Explains dependsOn's dual role (deps + expect/actual visibility) and the highest-resolvable-set placement rule.

for a principal

Designs hierarchy templates and custom groupings to minimize duplication and keeps placement rules consistent across a large KMP codebase.

## What an intermediate source set is Between `commonMain` (all targets) and the leaf platform source sets (`iosArm64Main`, `macosArm64Main`, ...) you can insert an **intermediate source set** such as `appleMain` (or `iosMain`, `nativeMain`). It groups targets that share more than `commonMain` does — e.g. all Apple targets share Darwin/Foundation interop and certain libraries. ## dependsOn wires the hierarchy `dependsOn` declares the parent relationship: ```kotlin kotlin { iosArm64(); macosArm64() sourceSets { val commonMain by getting val appleMain by creating { dependsOn(commonMain) } val iosArm64Main by getting { dependsOn(appleMain) } val macosArm64Main by getting { dependsOn(appleMain) } appleMain.dependencies { // a library available on all Apple/native targets, declared ONCE implementation("io.ktor:ktor-client-darwin:3.0.0") } } } ``` `dependsOn` does two things: 1. **Dependency inheritance** — deps declared on `appleMain` are visible to `iosArm64Main` and `macosArm64Main`. 2. **Symbol/`expect`-`actual` visibility** — code in `appleMain` may use the platform APIs common to its descendants (the intersection of the leaf platforms' APIs) and can provide `actual` declarations for `expect`s in `commonMain` that are shared by those targets. ## Where to declare a dependency Rule: **declare a dependency at the highest source set whose every descendant target can resolve it.** - A truly multiplatform artifact → `commonMain`. - An Apple/native-only artifact → `appleMain` (so iOS + macOS share it, but JVM/JS don't see it). - A single-target-only artifact → that leaf source set (`iosArm64Main`). Putting `ktor-client-darwin` in `commonMain` would break JVM/JS compilation; repeating it in each Apple leaf is redundant and error-prone. `appleMain` is the correct level. ## Default hierarchy template Kotlin 1.9.20+ enables the **default hierarchy template**: declaring `iosArm64()`, `iosX64()`, `macosArm64()` auto-creates `appleMain`, `iosMain`, `nativeMain` with the right `dependsOn` edges. You then just call `appleMain.dependencies { }` via the generated accessor and rarely wire `dependsOn` by hand. Manual `creating` + `dependsOn` is for custom groupings the template doesn't provide (e.g. a `jvmAndAndroidMain`). ## Pitfalls - Declaring a dep too low (each leaf) → duplication and drift. - Declaring it too high (commonMain) → unresolvable on non-matching targets. - Forgetting `dependsOn` on a hand-made set → the intermediate set is orphaned; its deps/symbols don't reach leaves and `expect`/`actual` won't link.

  • Why not just put the Darwin library in commonMain?
    commonMain is compiled for every target including JVM/JS, which have no Darwin variant, so resolution/compilation would fail there.
  • What does the default hierarchy template save you from writing?
    It auto-creates appleMain/iosMain/nativeMain and their dependsOn edges from the declared targets, so you only call the generated accessor instead of manual creating + dependsOn.

saying these in an interview costs you the question

  • Thinking dependsOn only affects compilation order, not dependency/symbol inheritance
  • Putting Apple-only deps in commonMain
  • Duplicating the same dep in every leaf instead of an intermediate set
  • Creating an intermediate set but forgetting dependsOn, leaving it orphaned
  • Not knowing the default hierarchy template auto-generates appleMain

context