Walk through designing a custom jvmAndAndroidMain intermediate source set: when is it worth it, how do you wire it, and what are the pitfalls?
answer
- Worth it when JVM+Android share java.* code
- Template won't auto-create it
- Wire: leaf dependsOn group dependsOn commonMain
- Surface = JVM∩Android: java.* yes, android.* no
- Prefer template group DSL over raw dependsOn
basics
~20 sCreate it when JVM and Android share a lot of java.*-based code you don't want to duplicate. Wire it with dependsOn so jvmMain and androidMain both inherit from it, and it depends on commonMain. Watch out for Android-only APIs leaking in and template conflicts.
solid answer
~40 sA `jvmAndAndroidMain` set is worth introducing when the JVM and Android targets share substantial `java.*`-based logic (e.g. `java.time`, `java.io`, SLF4J) that you'd otherwise duplicate in `jvmMain` and `androidMain`. The default hierarchy template does **not** create it, so you wire it manually: create the set with `dependsOn(commonMain)`, then make both `jvmMain` and `androidMain` `dependsOn(jvmAndAndroidMain)`. Its visible API surface is the JVM∩Android intersection — `java.*` is available, but **Android-framework** APIs (`android.*`) are **not**, since plain JVM lacks them; referencing them there fails. Pitfalls: mixing manual wiring with the template on the same nodes can warn/conflict; you must keep the set free of Android-only and JVM-only symbols; and test source sets need the parallel `jvmAndAndroidTest`. The cleaner modern approach is to extend the template via its DSL lambda rather than raw `dependsOn`.
code
kotlin · 14 lineskotlin {
jvm()
androidTarget()
applyDefaultHierarchyTemplate()
sourceSets {
val commonMain by getting
val jvmAndAndroidMain by creating { dependsOn(commonMain) }
val jvmMain by getting { dependsOn(jvmAndAndroidMain) }
val androidMain by getting { dependsOn(jvmAndAndroidMain) }
jvmAndAndroidMain.dependencies {
implementation("org.slf4j:slf4j-api:2.0.13") // java.* logging shared once
}
}
}go deeper
Knows JVM and Android can share code but cannot wire the set or reason about visibility.
Can wire jvmAndAndroidMain with dependsOn and knows java.* is shared.
Reasons about the JVM∩Android intersection, test-set names, and template-conflict pitfalls.
Decides when the extra set is justified, prefers the declarative template DSL, and governs the team's source-set conventions.
## When it earns its keep JVM and Android both run on a JVM bytecode runtime and share the **`java.*`** standard library. If your shared module has meaningful logic built on `java.time`, `java.io`, `java.util.concurrent`, or JVM-only libraries (SLF4J, OkHttp), you'd otherwise copy it into both `jvmMain` and `androidMain`. A `jvmAndAndroidMain` intermediate set holds that logic **once**. Skip it when there is little shared JVM-specific code — extra source sets add cognitive and build overhead. ## Wiring it manually The default hierarchy template deliberately keeps `jvmMain` and `androidMain` separate (Android is not a generic JVM platform), so you add the grouping yourself: ```kotlin kotlin { jvm() androidTarget() iosArm64() applyDefaultHierarchyTemplate() sourceSets { val commonMain by getting val jvmAndAndroidMain by creating { dependsOn(commonMain) } val jvmMain by getting { dependsOn(jvmAndAndroidMain) } val androidMain by getting { dependsOn(jvmAndAndroidMain) } // mirror for tests val commonTest by getting val jvmAndAndroidTest by creating { dependsOn(commonTest) } val jvmTest by getting { dependsOn(jvmAndAndroidTest) } val androidUnitTest by getting { dependsOn(jvmAndAndroidTest) } } } ``` ## The cleaner alternative: extend the template Rather than raw `dependsOn`, pass a lambda to `applyDefaultHierarchyTemplate { group("jvmAndAndroid") { withJvm(); withAndroidTarget() } }` (template group DSL), so the set is declared declaratively and stays consistent with the rest of the tree. ## API visibility there `jvmAndAndroidMain` is compiled for **both** JVM and Android, so its API surface is the **intersection**: - ✅ `java.*`, `kotlin.jvm.*`, JVM libraries. - ❌ `android.*` framework APIs — plain JVM lacks them, so referencing `android.content.Context` here won't compile. Those belong in `androidMain`. - ❌ JVM-only desktop APIs not on Android (rare, but `java.awt`/Swing) — Android lacks them. ## Pitfalls checklist - **Template/manual conflict**: don't both manually wire and let the template own the same node; prefer the template lambda. Mixing can emit `dependsOn` conflict warnings. - **Test sets**: remember the Android unit-test source set is `androidUnitTest` (and `androidInstrumentedTest`), not `androidTest`, when wiring the parallel test hierarchy. - **Leaky APIs**: a symbol available on JVM but not Android (or vice versa) used in the shared set produces an unresolved-reference error — keep it strictly to the intersection. - **Dependency scoping**: add shared JVM libs in `jvmAndAndroidMain.dependencies { implementation(...) }`, not in each leaf, to avoid duplication. ## Payoff Done right, you write `java.time`-based domain logic once, both targets inherit it, and Android-specific bits stay isolated in `androidMain`. This is the canonical real-world reason to hand-craft an intermediate set.
- Can you reference android.content.Context in jvmAndAndroidMain?No. The set is also compiled for plain JVM, which has no android.* framework, so it is outside the intersection; that code belongs in androidMain.
- What is the correct Android test source set to wire into jvmAndAndroidTest?androidUnitTest (host JVM unit tests). androidInstrumentedTest runs on a device and is separate; the legacy name androidTest no longer matches the current convention.
- What is a cleaner way to add this grouping than manual dependsOn?Use the default hierarchy template's group DSL lambda (group("jvmAndAndroid") { withJvm(); withAndroidTarget() }) so it stays consistent with the generated tree.
saying these in an interview costs you the question
- Putting android.* APIs in the shared JVM+Android set
- Wiring androidTest instead of androidUnitTest for host tests
- Double-wiring template-owned nodes manually, causing conflicts
- Duplicating shared JVM dependencies in both leaf sets
- Adding the set when there is no real shared JVM code