L’interface est localisée ; les présentations et documents des Skills sont traduits automatiquement par IA dans votre langue. Le code, les commandes, chemins et termes techniques restent en version originale, et le document de l’auteur fait foi.
Explorer les Skills
Design et création · SKILL.md

kotlin-multiplatform

kotlin-multiplatform

Kotlin Multiplatform (KMP) — share Kotlin code across Android, iOS, JVM Desktop, and JS/Wasm. Covers Gradle setup, source set hierarchy, expect/actual, target configuration, kotlinx-serialization, ktor-client, SQLDelight, multiplatform resources, and iOS framework export. USE WHEN: user mentions "Kotlin Multiplatform", "KMP", "expect/actual", "shared modul…

Independent GitHub snapshot · Automated classification · Original documentation governs

Documentation de la Skill

GitHub

name: kotlin-multiplatform description: | Kotlin Multiplatform (KMP) — share Kotlin code across Android, iOS, JVM Desktop, and JS/Wasm. Covers Gradle setup, source set hierarchy, expect/actual, target configuration, kotlinx-serialization, ktor-client, SQLDelight, multiplatform resources, and iOS framework export.

USE WHEN: user mentions "Kotlin Multiplatform", "KMP", "expect/actual", "shared module", "cross-platform Kotlin", "iOSMain", "commonMain", "Kotlin/Native", "kotlinx-serialization", "SQLDelight"

DO NOT USE FOR: Compose UI cross-platform - use frontend-frameworks/compose-multiplatform DO NOT USE FOR: Pure Kotlin language features - use languages/kotlin DO NOT USE FOR: Jetpack Compose Android-only - use mobile/jetpack-compose DO NOT USE FOR: Rust ↔ KMP bindings - use languages/uniffi allowed-tools: Read, Grep, Glob, Write, Edit

Kotlin Multiplatform

References: gradle.md for full Gradle config, source set hierarchy, target setup. ios-integration.md for iOS framework export, CocoaPods, SwiftPM, Xcode integration. libraries.md for ktor, kotlinx-serialization, SQLDelight, Koin patterns in KMP.

Deep Knowledge: Use mcp__documentation__fetch_docs with technology: kotlin-multiplatform.

What KMP Solves

Share business logic (networking, persistence, domain, view models) across platforms. Each platform retains native UI:

shared/                                        # commonMain — Kotlin code shared everywhere
├── domain                                     # Models, use cases
├── data                                       # Repositories, DTOs, mappers
├── network                                    # Ktor client
└── persistence                                # SQLDelight queries

apps/
├── android (Kotlin + Jetpack Compose)         # uses shared
├── ios (Swift + SwiftUI)                      # uses shared via XCFramework
└── desktop (Kotlin + Compose Desktop)         # uses shared

KMP is not "write once run anywhere" — UI stays native (or use Compose Multiplatform for shared UI).

Module Structure

shared/
├── build.gradle.kts
└── src/
    ├── commonMain/kotlin/                     # platform-agnostic code
    │   └── com/example/Wallet.kt
    ├── commonTest/kotlin/                     # shared tests
    │
    ├── androidMain/kotlin/                    # Android-specific
    │   └── com/example/AndroidPlatform.kt
    ├── androidUnitTest/kotlin/
    │
    ├── iosMain/kotlin/                        # iOS-specific (all iOS targets)
    │   └── com/example/IosPlatform.kt
    ├── iosTest/kotlin/
    │
    ├── desktopMain/kotlin/                    # JVM Desktop
    └── jsMain/kotlin/                         # Browser/Node (optional)

expect / actual

The cross-platform mechanism. expect declares an API in commonMain; each target provides actual implementation.

Functions

// commonMain
expect fun platformName(): String
expect fun openUrl(url: String)
// androidMain
import android.content.Intent
import android.net.Uri

actual fun platformName(): String = "Android ${android.os.Build.VERSION.SDK_INT}"

actual fun openUrl(url: String) {
    val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
        .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
    appContext.startActivity(intent)
}
// iosMain
import platform.UIKit.UIApplication
import platform.Foundation.NSURL

actual fun platformName(): String = "iOS ${UIDevice.currentDevice.systemVersion}"

actual fun openUrl(url: String) {
    NSURL.URLWithString(url)?.let {
        UIApplication.sharedApplication.openURL(it)
    }
}

Classes

// commonMain
expect class SecureStorage {
    fun put(key: String, value: ByteArray)
    fun get(key: String): ByteArray?
    fun delete(key: String)
}
// androidMain — Android Keystore-backed
actual class SecureStorage(private val context: Context) {
    actual fun put(key: String, value: ByteArray) { /* AndroidX EncryptedSharedPreferences */ }
    actual fun get(key: String): ByteArray? { /* ... */ }
    actual fun delete(key: String) { /* ... */ }
}
// iosMain — Keychain-backed
import platform.Security.*

actual class SecureStorage {
    actual fun put(key: String, value: ByteArray) { /* SecItemAdd */ }
    actual fun get(key: String): ByteArray? { /* SecItemCopyMatching */ }
    actual fun delete(key: String) { /* SecItemDelete */ }
}

Type Aliases (lightweight expect)

For simple wrappers around platform types:

// commonMain
expect class UUID

// androidMain
actual typealias UUID = java.util.UUID

// iosMain
actual typealias UUID = platform.Foundation.NSUUID

Source Set Hierarchy (Default)

KMP 1.9+ uses a default hierarchy template:

commonMain
├── androidMain
├── jvmMain        (desktop)
├── jsMain
├── nativeMain
│   ├── linuxMain
│   ├── mingwMain  (Windows)
│   ├── appleMain
│   │   ├── iosMain
│   │   │   ├── iosX64Main
│   │   │   ├── iosArm64Main
│   │   │   └── iosSimulatorArm64Main
│   │   ├── macosMain
│   │   ├── tvosMain
│   │   └── watchosMain

Code in appleMain is shared across all Apple targets. Code in iosMain only across iOS targets. Useful for Apple-wide APIs (Keychain, NSURLSession) vs iOS-specific (UIKit).

// Custom intermediate source set (rare)
kotlin {
    sourceSets {
        val mobileMain by creating {
            dependsOn(getByName("commonMain"))
        }
        getByName("androidMain").dependsOn(mobileMain)
        getByName("iosMain").dependsOn(mobileMain)
    }
}

Minimal Gradle Setup

// shared/build.gradle.kts
plugins {
    kotlin("multiplatform") version "2.2.0"
    id("com.android.library") version "8.7.0"
    kotlin("plugin.serialization") version "2.2.0"
}

kotlin {
    androidTarget {
        compilations.all {
            kotlinOptions { jvmTarget = "17" }
        }
    }

    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach { target ->
        target.binaries.framework {
            baseName = "Shared"
            isStatic = true
        }
    }

    jvm("desktop")

    sourceSets {
        commonMain.dependencies {
            implementation("io.ktor:ktor-client-core:3.0.0")
            implementation("io.ktor:ktor-client-content-negotiation:3.0.0")
            implementation("io.ktor:ktor-serialization-kotlinx-json:3.0.0")
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.10.0")
            implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.0")
            implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
        }
        androidMain.dependencies {
            implementation("io.ktor:ktor-client-okhttp:3.0.0")
        }
        iosMain.dependencies {
            implementation("io.ktor:ktor-client-darwin:3.0.0")
        }
        commonTest.dependencies {
            implementation(kotlin("test"))
            implementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.10.0")
        }
    }
}

android {
    namespace = "com.example.shared"
    compileSdk = 35
    defaultConfig {
        minSdk = 26
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_17
        targetCompatibility = JavaVersion.VERSION_17
    }
}

See gradle.md for advanced setup (custom intermediate source sets, version catalogs, Compose KMP, multiplatform resources).

iOS Framework Export

listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach { target ->
    target.binaries.framework {
        baseName = "Shared"
        isStatic = true     // recommended — smaller bundle, faster link
        embedBitcode("disable")
    }
}

Build:

./gradlew :shared:linkReleaseFrameworkIosArm64
./gradlew :shared:linkReleaseFrameworkIosSimulatorArm64

To create a single XCFramework consumable by Xcode:

val xcf = XCFramework("Shared")
listOf(iosX64(), iosArm64(), iosSimulatorArm64()).forEach {
    it.binaries.framework {
        baseName = "Shared"
        xcf.add(this)
    }
}

// Task: ./gradlew :shared:assembleSharedXCFramework

For SwiftPM consumption, see ios-integration.md.

Networking — Ktor Client

// commonMain
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json

@Serializable
data class User(val id: Long, val name: String)

class UserApi(private val client: HttpClient) {
    suspend fun getUser(id: Long): User =
        client.get("https://api.example.com/users/$id").body()
}

// httpClient construction with platform engine
expect fun createHttpClient(): HttpClient

// commonMain helper
fun makeClient(): HttpClient = HttpClient(engine = httpClientEngine()) {
    install(ContentNegotiation) {
        json(Json { ignoreUnknownKeys = true })
    }
}
// androidMain
import io.ktor.client.engine.okhttp.OkHttp
actual fun httpClientEngine() = OkHttp.create { /* ... */ }

// iosMain
import io.ktor.client.engine.darwin.Darwin
actual fun httpClientEngine() = Darwin.create { /* ... */ }

Persistence — SQLDelight

// commonMain
import app.cash.sqldelight.db.SqlDriver

expect class DriverFactory {
    fun createDriver(): SqlDriver
}

class WalletDatabase(driver: SqlDriver) {
    val database = AppDatabase(driver)
    val txQueries = database.transactionQueries
}
// androidMain
import app.cash.sqldelight.driver.android.AndroidSqliteDriver

actual class DriverFactory(private val context: Context) {
    actual fun createDriver(): SqlDriver =
        AndroidSqliteDriver(AppDatabase.Schema, context, "wallet.db")
}

// iosMain
import app.cash.sqldelight.driver.native.NativeSqliteDriver

actual class DriverFactory {
    actual fun createDriver(): SqlDriver =
        NativeSqliteDriver(AppDatabase.Schema, "wallet.db")
}

SQL files in commonMain/sqldelight/com/example/AppDatabase.sq:

CREATE TABLE Transaction (
    id TEXT PRIMARY KEY,
    amount INTEGER NOT NULL,
    timestamp INTEGER NOT NULL
);

selectAll:
SELECT * FROM Transaction ORDER BY timestamp DESC;

insert:
INSERT INTO Transaction(id, amount, timestamp) VALUES (?, ?, ?);

Generated Kotlin code is type-safe and async-aware (returns Flow with asFlow() extension).

DI — Koin Multiplatform

// commonMain
val sharedModule = module {
    single { createHttpClient() }
    single { UserApi(get()) }
    single { DriverFactory(get()) }
    single { WalletDatabase(get<DriverFactory>().createDriver()) }
}

fun initKoin(extraModules: List<Module> = emptyList()) = startKoin {
    modules(sharedModule + extraModules)
}

Android side passes Context:

// AndroidMainActivity
initKoin(extraModules = listOf(module {
    single<Context> { applicationContext }
}))

iOS calls initKoin() from Swift via the generated bridge.

Multiplatform Resources

KMP supports shared resources (images, fonts, strings) via compose-multiplatform-resources (works without Compose too):

shared/src/commonMain/composeResources/
├── drawable/
│   └── logo.png
├── values/
│   └── strings.xml
└── font/
    └── inter.ttf

Access from Kotlin:

import org.jetbrains.compose.resources.painterResource
import org.jetbrains.compose.resources.stringResource

@Composable
fun Logo() {
    Image(painterResource(Res.drawable.logo), contentDescription = null)
}

For non-Compose code use Res.readBytes("files/data.json").

Anti-Patterns

Anti-patternWhy it's badCorrect approach
Putting everything in commonMainBloats unused code on each platformSplit by feature; use intermediate source sets
Using Java.util.* in commonMainNot available on iOS/JSUse kotlinx-datetime, kotlinx.io, etc.
Synchronous network in commonMainBlocks platform threadsUse Ktor + suspend
Forgetting to add iosMain ktor engineRuntime crash on iOSEach platform needs its engine actualized
expect class X(arg: Type) with constructor mismatchCompile errorMatch constructor signatures across actuals
lateinit var on multiplatform shared stateNative targets don't enforce same wayUse nullable + check
Heavy serialization in commonMain without flowOnBlocks UI on iOS mainUse Dispatchers.Default

Build & Distribution

TargetOutputHow consumers use it
Android.aar libraryGradle dependency
iOS deviceShared.frameworkEmbed in Xcode project
iOS sim+device combinedShared.xcframeworkSingle artifact for SwiftPM/CocoaPods
JVM Desktop.jarGradle/Maven dependency
JS/Wasm.js packagenpm
# Common Gradle tasks
./gradlew :shared:assemble                    # build all targets
./gradlew :shared:assembleSharedXCFramework   # iOS XCFramework
./gradlew :shared:packForXcode                # CocoaPods integration
./gradlew :shared:publishToMavenLocal         # local Maven for testing

Testing

// commonTest
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlinx.coroutines.test.runTest

class UserApiTest {
    @Test fun `parses user response`() = runTest {
        val api = UserApi(mockClient(""" {"id":1,"name":"Alice"} """))
        val user = api.getUser(1)
        assertEquals("Alice", user.name)
    }
}

Tests run on all targets by default. Use expect class TestUtil for platform-specific test helpers (mock HTTP engines).

When NOT to Use This Skill

ScenarioUse Instead
Compose UI shared across platformsfrontend-frameworks/compose-multiplatform
Android Jetpack Compose onlymobile/jetpack-compose
iOS native SwiftUImobile/ios-native (when added) + languages/swift
Pure Kotlin language patternslanguages/kotlin
Bridging Rust to KMPlanguages/uniffi (KMP fork section)
KMP iOS framework export detailsios-integration.md
Skills enregistrées