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
| Library | Purpose | Common API |
|---|---|---|
| Ktor Client | HTTP networking | HttpClient, get(), post() |
| kotlinx.serialization | JSON/CBOR serialization | @Serializable, Json.decodeFromString() |
| kotlinx.coroutines | Async programming | launch, async, flow |
| SQLDelight | Local database | Type-safe SQL queries |
| Koin | Dependency injection | module { }, single { } |
| kotlinx.datetime | Date/time handling | Clock.System.now() |
| Napier | Logging | Napier.d(), Napier.e() |
| Multiplatform Settings | Key-value storage | Settings().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
| Issue | Solution |
|---|---|
| iOS build fails with framework error | Run ./gradlew :shared:linkDebugFrameworkIosSimulatorArm64 manually |
| ”Unresolved reference” in platform code | Check source set names match exactly (e.g., iosMain not iosNativeMain) |
| CocoaPods integration fails | Run pod repo update then pod install in iosApp directory |
| Gradle sync slow | Enable Gradle configuration cache in gradle.properties |
| ”No matching variant” error | Ensure all platforms define the same expect/actual declarations |
| iOS memory leaks | Avoid circular references; use weak in Objective-C interop |
| Coroutines crash on iOS | Use Dispatchers.Main carefully; configure native dispatcher |
| WASM target compilation errors | Ensure using wasmJs not deprecated wasm target |
| Xcode cannot find framework | Clean build: ./gradlew clean then rebuild framework |
| Version conflict between platforms | Use Gradle version catalog (libs.versions.toml) for consistency |