kotlin-multiplatform

v2026.09.24

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`

GitHub
安装命令
npx skhub add claude-dev-suite/kotlin-multiplatform
Markdown
SKILL.md

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
发现
标签

此技能尚未发布标签。

版本
最新版本元数据

版本

v2026.09.24

发布时间

Sep 24, 2026

分类

未分类

许可证

MIT

源路径

skills/mobile/kotlin-multiplatform

默认分支

main

最新提交

9496306

Tree SHA

fe4e2f1