skip to content

How is a Kotlin Multiplatform library published and consumed, and what role does Gradle variant-aware resolution play?

level: seniorimportance: should knowfreq 30%

answer

  1. root metadata module + per-target modules
  2. Gradle Module Metadata (.module) required
  3. attributes: platform.type jvm/native/js
  4. variant-aware attribute matching selects artifact
  5. declare dependency once in commonMain

basics

~20 s

A KMP library publishes a root metadata module plus one Gradle module per target (jvm, iosArm64, js…), each with attributes. When a consumer adds the library, Gradle's variant-aware resolution matches each target's attributes to pick the correct per-platform artifact automatically.

solid answer

~40 s

Applying `maven-publish` to a KMP module makes it publish a **root (metadata) module** plus a separate Gradle module for each declared target. Each published module carries Gradle Module Metadata (`.module` files) with **attributes** like `org.gradle.usage`, `org.jetbrains.kotlin.platform.type`, and native target attributes. A consumer just declares the dependency once (typically in `commonMain.dependencies`); Gradle's **variant-aware resolution** then matches the *consuming* target's attributes against the published variants and selects the right artifact for the JVM compilation, the iOS compilation, and so on. This is why `implementation("io.ktor:ktor-client-core:2.3.0")` in `commonMain` transparently resolves to `ktor-client-core-jvm` for the JVM target and the native klib for iOS. The root module's metadata is what makes the per-target fan-out work; without Gradle Module Metadata enabled, multiplatform resolution breaks down to plain POMs and loses target matching.

code

kotlin · 19 lines
kotlin
plugins {
    kotlin("multiplatform") version "2.0.0"
    `maven-publish`
}

kotlin {
    jvm()
    iosArm64()
    js(IR) { browser() }
}

// publishing fans out automatically:
//   com.acme:lib            (root + Gradle Module Metadata)
//   com.acme:lib-jvm
//   com.acme:lib-iosarm64
//   com.acme:lib-js
publishing {
    repositories { mavenLocal() }
}

go deeper

for a junior

Knowing you add the dependency once and it 'just works' per platform is acceptable.

for a middle

Explain that publishing produces a root module plus per-target modules and the consumer uses the root coordinates.

for a senior

Describe Gradle Module Metadata, attribute-based variant matching, and the GMM-required failure mode.

for a principal

Address repository/registry GMM support, version alignment across variants, API-stability governance of the common surface, and the org-wide implications of shipping multiplatform libraries.

## What gets published A single KMP module is *not* published as a single jar. With `maven-publish` applied, the plugin publishes: 1. A **root module** (the "metadata"/`kotlinMultiplatform` publication) at the library's main coordinates, e.g. `io.ktor:ktor-client-core`. It contains the common metadata klib and, crucially, **Gradle Module Metadata** pointing at the per-target modules. 2. One **target module** per declared target: `io.ktor:ktor-client-core-jvm`, `…-iosarm64`, `…-js`, etc. Each holds that platform's real artifact (jar, klib, …) and its own metadata. ## Attributes and variants Gradle's dependency model is **variant-aware**: every producer variant and every consumer request is tagged with *attributes*. KMP publications carry attributes such as: - `org.gradle.usage` (`kotlin-api`, `kotlin-runtime`, …), - `org.jetbrains.kotlin.platform.type` (`jvm`, `native`, `js`, `common`), - native-specific attributes identifying the konan target. When a consumer compilation resolves dependencies, Gradle performs **attribute matching**: the JVM compilation requests `platform.type = jvm`, so it selects the `-jvm` variant; the iOS compilation requests the matching native target and selects the corresponding klib. The developer writes the dependency *once* in `commonMain` and resolution fans it out per target. ## Why Gradle Module Metadata is mandatory Maven POMs cannot express variants — they are a flat dependency list. KMP relies on **Gradle Module Metadata** (`.module` JSON files, enabled by default in modern Gradle) to encode the variant graph. If GMM is disabled or a repository strips it, a consumer falls back to the POM and can't resolve the right per-target artifact, producing errors like "could not resolve … no matching variant". So publishing KMP libraries requires GMM and a repository that preserves it. ## Consuming ```kotlin kotlin { jvm(); iosArm64() sourceSets { commonMain.dependencies { implementation("io.ktor:ktor-client-core:2.3.0") // root coords } } } ``` The consumer references only the root coordinates. Gradle resolves `-jvm` for the JVM target and the iOS klib for `iosArm64` automatically. Consuming a KMP library from a *plain* `kotlin("jvm")` module also works: that single JVM consumer simply matches the `-jvm` variant. ## Design implications - **Repository choice**: Maven Central works because it preserves GMM; ensure your repo does too. - **Version alignment**: all per-target modules share one version, so the library is consumed as a coherent unit. - **API surface**: the common artifact defines the shared API; platform artifacts provide `actual` implementations. Breaking the common API breaks every consumer regardless of target. From a build-engineering and system-design perspective, the key insight is that KMP leans entirely on Gradle's variant model: *one dependency declaration, attribute-matched to many published artifacts*. That is also the bridge point — the deep `expect`/`actual` semantics are a Kotlin-language concern, but the resolution mechanics are pure Gradle.

  • What happens if Gradle Module Metadata is disabled or stripped by a repository?
    Consumers fall back to the flat POM, which cannot describe variants, so target matching fails — typically with a 'no matching variant'/'could not resolve' error. KMP publishing therefore requires GMM and a repository that preserves the `.module` files.
  • Can a plain `kotlin("jvm")` module consume a KMP library?
    Yes. The JVM-only consumer requests the `jvm` platform attribute and Gradle selects the `-jvm` variant of the library, ignoring the other targets. The root coordinates are still what you declare.

saying these in an interview costs you the question

  • Saying a KMP library is one fat jar covering all platforms.
  • Claiming Maven POMs alone can express multiplatform variants.
  • Thinking you must depend on the `-jvm`/`-iosarm64` coordinates directly instead of the root coordinates.

context