콘텐츠로 이동

Kotlin Multiplatform Cheat Sheet

Overview

Kotlin Multiplatform (KMP) is a technology from JetBrains that enables sharing Kotlin code across multiple platforms including Android, iOS, desktop (JVM), web (JavaScript/WASM), and server. Unlike cross-platform frameworks that abstract away the platform, KMP allows developers to share business logic, data layers, and networking code while keeping the UI native to each platform. This approach yields the best of both worlds: code reuse for non-UI logic and fully native user experiences with platform-specific APIs.

KMP uses an expect/actual mechanism for platform-specific implementations, allowing you to define common interfaces in shared code and provide platform-specific implementations where needed. The ecosystem includes libraries like Ktor for networking, SQLDelight for databases, and kotlinx.serialization for JSON parsing, all designed to work cross-platform. Compose Multiplatform extends Jetpack Compose to iOS, desktop, and web, enabling optional UI sharing as well.

Installation

# Install JDK 17+
brew install openjdk@17    # macOS
sudo apt install openjdk-17-jdk  # Ubuntu/Debian

# Install Android Studio with KMP plugin
# Or use IntelliJ IDEA with Kotlin Multiplatform plugin

# Install Xcode (required for iOS targets)
xcode-select --install

# Install CocoaPods (for iOS dependency management)
sudo gem install cocoapods

# Create new KMP project using wizard
# https://kmp.jetbrains.com
# Or use the KMP project template in Android Studio
# Verify setup
./gradlew --version
kotlin -version
xcodebuild -version

Project Structure

my-kmp-project/
├── build.gradle.kts              # Root build config
├── settings.gradle.kts
├── composeApp/                   # Compose Multiplatform app (optional)
│   └── src/
│       ├── commonMain/           # Shared Compose UI
│       ├── androidMain/          # Android-specific UI
│       └── iosMain/              # iOS-specific UI
├── shared/                       # Shared business logic
│   └── src/
│       ├── commonMain/kotlin/    # Shared Kotlin code
│       ├── commonTest/kotlin/    # Shared tests
│       ├── androidMain/kotlin/   # Android implementations
│       ├── iosMain/kotlin/       # iOS implementations
│       ├── jvmMain/kotlin/       # Desktop implementations
│       └── wasmJsMain/kotlin/    # Web implementations
├── androidApp/                   # Android entry point
├── iosApp/                       # iOS Xcode project
└── gradle/
    └── libs.versions.toml        # Version catalog

Gradle Configuration

// shared/build.gradle.kts
plugins {
    alias(libs.plugins.kotlinMultiplatform)
    alias(libs.plugins.androidLibrary)
    alias(libs.plugins.kotlinSerialization)
}

kotlin {
    androidTarget {
        compilations.all {
            compileTaskProvider.configure {
                compilerOptions {
                    jvmTarget.set(JvmTarget.JVM_17)
                }
            }
        }
    }
    
    listOf(
        iosX64(),
        iosArm64(),
        iosSimulatorArm64()
    ).forEach {
        it.binaries.framework {
            baseName = "shared"
            isStatic = true
        }
    }
    
    jvm("desktop")
    
    wasmJs {
        browser()
    }
    
    sourceSets {
        commonMain.dependencies {
            implementation(libs.kotlinx.coroutines.core)
            implementation(libs.kotlinx.serialization.json)
            implementation(libs.ktor.client.core)
            implementation(libs.ktor.client.content.negotiation)
            implementation(libs.ktor.serialization.json)
        }
        commonTest.dependencies {
            implementation(libs.kotlin.test)
        }
        androidMain.dependencies {
            implementation(libs.ktor.client.android)
        }
        iosMain.dependencies {
            implementation(libs.ktor.client.darwin)
        }
        named("desktopMain").dependencies {
            implementation(libs.ktor.client.cio)
        }
    }
}

android {
    namespace = "com.example.shared"
    compileSdk = 35
    defaultConfig { minSdk = 24 }
}

Shared Code (commonMain)

// commonMain/kotlin/com/example/Platform.kt
expect class Platform() {
    val name: String
    val version: String
}

// commonMain/kotlin/com/example/data/Repository.kt
class UserRepository(private val api: ApiClient) {
    suspend fun getUsers(): List<User> {
        return api.fetchUsers()
    }
    
    suspend fun getUserById(id: Int): User {
        return api.fetchUser(id)
    }
}

// commonMain/kotlin/com/example/model/User.kt
@Serializable
data class User(
    val id: Int,
    val name: String,
    val email: String
)

// commonMain/kotlin/com/example/network/ApiClient.kt
class ApiClient(engine: HttpClientEngine) {
    private val client = HttpClient(engine) {
        install(ContentNegotiation) {
            json(Json { ignoreUnknownKeys = true })
        }
    }
    
    suspend fun fetchUsers(): List<User> {
        return client.get("https://api.example.com/users").body()
    }
}

Platform-Specific Implementations

// androidMain/kotlin/com/example/Platform.android.kt
actual class Platform actual constructor() {
    actual val name: String = "Android"
    actual val version: String = "${android.os.Build.VERSION.SDK_INT}"
}

// iosMain/kotlin/com/example/Platform.ios.kt
import platform.UIKit.UIDevice

actual class Platform actual constructor() {
    actual val name: String = UIDevice.currentDevice.systemName()
    actual val version: String = UIDevice.currentDevice.systemVersion
}

// desktopMain/kotlin/com/example/Platform.desktop.kt
actual class Platform actual constructor() {
    actual val name: String = "Desktop"
    actual val version: String = System.getProperty("os.version") ?: "unknown"
}

Expect/Actual Pattern

// commonMain - declare expected interface
expect fun randomUUID(): String

expect fun platformLog(tag: String, message: String)

expect class SecureStorage() {
    fun save(key: String, value: String)
    fun read(key: String): String?
    fun delete(key: String)
}

// androidMain - provide Android implementation
actual fun randomUUID(): String = java.util.UUID.randomUUID().toString()

actual fun platformLog(tag: String, message: String) {
    android.util.Log.d(tag, message)
}

actual class SecureStorage actual constructor() {
    private val prefs = EncryptedSharedPreferences.create(/*...*/)
    actual fun save(key: String, value: String) { prefs.edit().putString(key, value).apply() }
    actual fun read(key: String): String? = prefs.getString(key, null)
    actual fun delete(key: String) { prefs.edit().remove(key).apply() }
}

// iosMain - provide iOS implementation
import platform.Foundation.NSUUID

actual fun randomUUID(): String = NSUUID().UUIDString()

actual fun platformLog(tag: String, message: String) {
    println("[$tag] $message")
}

Common Libraries

LibraryPurposeCommon API
Ktor ClientHTTP networkingHttpClient, get(), post()
kotlinx.serializationJSON/CBOR serialization@Serializable, Json.decodeFromString()
kotlinx.coroutinesAsync programminglaunch, async, flow
SQLDelightLocal databaseType-safe SQL queries
KoinDependency injectionmodule { }, single { }
kotlinx.datetimeDate/time handlingClock.System.now()
NapierLoggingNapier.d(), Napier.e()
Multiplatform SettingsKey-value storageSettings().putString()

Build and Run

# Build shared module
./gradlew :shared:build

# Run Android app
./gradlew :androidApp:installDebug

# Run iOS app (from iosApp directory)
cd iosApp && pod install
open iosApp.xcworkspace
# Build and run from Xcode

# Run desktop app
./gradlew :composeApp:run

# Run web app
./gradlew :composeApp:wasmJsBrowserRun

# Run all tests
./gradlew allTests

# Run platform-specific tests
./gradlew :shared:testDebugUnitTest       # Android
./gradlew :shared:iosSimulatorArm64Test    # iOS Simulator
./gradlew :shared:desktopTest              # Desktop

Advanced Usage

// Dependency injection with Koin
// commonMain
val sharedModule = module {
    single { Json { ignoreUnknownKeys = true } }
    single { ApiClient(get()) }
    single { UserRepository(get()) }
}

// Flow-based reactive patterns
class UserViewModel(private val repo: UserRepository) {
    private val _users = MutableStateFlow<List<User>>(emptyList())
    val users: StateFlow<List<User>> = _users.asStateFlow()
    
    fun loadUsers() {
        coroutineScope.launch {
            repo.getUsers()
                .catch { e -> /* handle error */ }
                .collect { _users.value = it }
        }
    }
}

// SQLDelight cross-platform database
// src/commonMain/sqldelight/com/example/db/AppDatabase.sq
CREATE TABLE User (
    id INTEGER PRIMARY KEY,
    name TEXT NOT NULL,
    email TEXT NOT NULL
);

selectAll:
SELECT * FROM User;

insertUser:
INSERT INTO User(id, name, email) VALUES (?, ?, ?);

Troubleshooting

IssueSolution
iOS build fails with framework errorRun ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 manually
”Unresolved reference” in platform codeCheck source set names match exactly (e.g., iosMain not iosNativeMain)
CocoaPods integration failsRun pod repo update then pod install in iosApp directory
Gradle sync slowEnable Gradle configuration cache in gradle.properties
”No matching variant” errorEnsure all platforms define the same expect/actual declarations
iOS memory leaksAvoid circular references; use weak in Objective-C interop
Coroutines crash on iOSUse Dispatchers.Main carefully; configure native dispatcher
WASM target compilation errorsEnsure using wasmJs not deprecated wasm target
Xcode cannot find frameworkClean build: ./gradlew clean then rebuild framework
Version conflict between platformsUse Gradle version catalog (libs.versions.toml) for consistency