skip to content

How do you declare dependencies in a KMP module so common code and individual platforms each get the right libraries?

level: seniorimportance: should knowfreq 50%

answer

  1. dependencies live per source set in kotlin { sourceSets }
  2. commonMain deps must be multiplatform (Gradle Module Metadata)
  3. platform deps in jvmMain/androidMain/iosMain
  4. dependsOn means common deps inherited
  5. JVM-only lib in commonMain = resolution failure

basics

~10 s

Add shared libraries inside commonMain's dependencies block (they must be multiplatform), and add platform-only libraries inside that platform's source-set dependencies block, like jvmMain or androidMain.

solid answer

~40 s

Dependencies are declared **per source set** under `kotlin { sourceSets { ... } }`. Put cross-platform libraries in `commonMain.dependencies { }` — they must be published with **multiplatform metadata** (Gradle resolves the right variant per target via Gradle Module Metadata). Platform-only libraries go in the matching platform set, e.g. `jvmMain.dependencies { implementation("...") }` for a JVM-only library, or `androidMain` for an Android library. Tests go in `commonTest` (usually `kotlin("test")`) and platform test sets. Because platform sets `dependsOn` common, a dependency added to `commonMain` is visible to every platform set, so you don't redeclare it. Configurations are the familiar `implementation` / `api` / `compileOnly` / `runtimeOnly`. A common pitfall is adding a JVM-only artifact (no `-jvm` multiplatform variant) to `commonMain`, which fails to resolve for non-JVM targets — it belongs in `jvmMain`.

code

kotlin · 15 lines
kotlin
kotlin {
    jvm()
    iosArm64()
    sourceSets {
        commonMain.dependencies {
            implementation("io.ktor:ktor-client-core:3.0.0")
        }
        jvmMain.dependencies {
            implementation("io.ktor:ktor-client-okhttp:3.0.0")
        }
        iosArm64Main.dependencies {
            implementation("io.ktor:ktor-client-darwin:3.0.0")
        }
    }
}

go deeper

for a junior

Knows shared libraries go in commonMain and platform-specific ones in the platform set.

for a middle

Explains dependsOn inheritance and the multiplatform-metadata requirement for common dependencies.

for a senior

Designs the core-API-in-common / engine-per-platform split and diagnoses variant-resolution failures.

for a principal

Sets dependency conventions (api vs implementation leakage, catalog usage) across a large multi-module KMP codebase and its publication.

## Dependencies are per source set In KMP you don't put dependencies in a top-level `dependencies { }` block — you attach them to **source sets** inside `kotlin { sourceSets { ... } }`: ```kotlin kotlin { jvm() js(IR) { nodejs() } sourceSets { commonMain.dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0") // multiplatform } jvmMain.dependencies { implementation("com.squareup.okhttp3:okhttp:4.12.0") // JVM-only } commonTest.dependencies { implementation(kotlin("test")) } } } ``` ## Common vs. platform dependencies - **commonMain** dependencies are visible to *every* target, so they must be **multiplatform-capable** — published with **Gradle Module Metadata** so Gradle can pick the per-target variant (e.g. `kotlinx-coroutines-core` resolves to `-jvm`, `-js`, `-iosarm64`, … automatically). Examples: `kotlinx-coroutines-core`, `kotlinx-serialization-json`, Ktor client core. - **Platform-set** dependencies (e.g. `jvmMain`, `androidMain`, `iosMain`) are visible only to that platform. Put a JVM-only library like OkHttp, or a platform Ktor engine (`ktor-client-okhttp` on JVM, `ktor-client-darwin` on iOS), here. ## Inheritance via dependsOn Because `jvmMain` `dependsOn(commonMain)`, anything in `commonMain.dependencies` is automatically available to `jvmMain` — never redeclare it. ## Configurations The usual Gradle configurations apply per source set: `implementation` (default, not exposed to consumers), `api` (exposed transitively), `compileOnly`, `runtimeOnly`. `api` in `commonMain` leaks the dependency to consumers of all platforms. ## The classic pitfall Adding a **JVM-only** artifact (one without multiplatform variants) to `commonMain` breaks resolution for non-JVM targets — Gradle can't find a `-js`/`-native` variant. The fix is to move it to `jvmMain`, or abstract behind `expect/actual` and supply per-platform libraries in each platform set. ## Engines / platform splits pattern A frequent shape: core API in `commonMain` (e.g. Ktor `client-core`), and the concrete **engine** chosen per platform in each platform set. This keeps shared code portable while binding to a platform-appropriate implementation. ## Version catalogs still apply You can reference `libs.kotlinx.coroutines` from a version catalog inside these blocks just as in any Gradle build, keeping versions centralized.

  • Why does adding a plain JVM jar to commonMain fail?
    commonMain compiles for all targets; a JVM-only artifact has no js/native variant, so Gradle Module Metadata resolution fails for those targets.
  • Do you need to redeclare a commonMain dependency in jvmMain?
    No. jvmMain dependsOn commonMain, so common dependencies are already visible to it.

commonMain dependencies are carry-on bags allowed on every airline (multiplatform); platform-only libraries are oversized items you can only bring on the specific airline (target) that accepts them.

saying these in an interview costs you the question

  • Putting all dependencies in a top-level dependencies { } block
  • Adding a JVM-only library to commonMain
  • Redeclaring common dependencies in every platform set
  • Not knowing multiplatform libs resolve per-target via Gradle Module Metadata

context