Kotlin Multiplatform Cheat Sheet
Covers Kotlin Multiplatform project structure, expect/actual declarations, shared source sets, and common targets for sharing code across platforms.
expect / actual Basics
Declaring a common API with platform-specific implementations.
// commonMain/kotlin/Platform.ktexpect class Platform() { val name: String}expect fun getPlatform(): Platform// androidMain/kotlin/Platform.android.ktactual class Platform actual constructor() { actual val name: String = "Android ${android.os.Build.VERSION.SDK_INT}"}actual fun getPlatform(): Platform = Platform()// iosMain/kotlin/Platform.ios.ktactual class Platform actual constructor() { actual val name: String = UIDevice.currentDevice.systemName()}actual fun getPlatform(): Platform = Platform()
Gradle Source Sets
Configuring targets and shared source sets in build.gradle.kts.
kotlin { androidTarget() iosX64() iosArm64() iosSimulatorArm64() jvm("desktop") sourceSets { val commonMain by getting { dependencies { implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0") } } val androidMain by getting val iosMain by creating { dependsOn(commonMain) } val iosX64Main by getting { dependsOn(iosMain) } val iosArm64Main by getting { dependsOn(iosMain) } }}
Common Compilation Targets
Platforms Kotlin Multiplatform can compile to.
- androidTarget()- Compiles Kotlin/JVM bytecode for Android
- jvm()- Compiles Kotlin/JVM for desktop or server targets
- iosX64() / iosArm64() / iosSimulatorArm64()- Compiles Kotlin/Native for iOS simulator and device
- js(IR)- Compiles Kotlin/JS using the modern IR compiler backend
- wasmJs()- Compiles Kotlin/Wasm for WebAssembly targets
- linuxX64() / macosX64() / mingwX64()- Kotlin/Native targets for desktop platforms
Common vs. Platform Code
Where shared logic and platform-only code each belong.
// commonMain: shared business logic, no platform APIsclass UserRepository(private val api: ApiClient) { suspend fun getUser(id: String) = api.fetchUser(id)}// commonMain can use expect declarations for anything platform-specific,// like file storage, HTTP engines (via Ktor's multiplatform client), or// platform-specific date/time formatting.// androidMain / iosMain: actual implementations, plus platform-only code// that isn't part of any expect declaration at all (e.g. Android Activity// classes or iOS SwiftUI views) lives entirely in its own source set.
Popular KMP Libraries
Widely used multiplatform-compatible libraries.
- Ktor Client- Multiplatform HTTP client with pluggable engines per platform
- kotlinx.serialization- Multiplatform JSON/CBOR/protobuf serialization
- kotlinx.coroutines- Multiplatform coroutines, including Flow, work across all KMP targets
- SQLDelight- Generates typed Kotlin APIs from SQL, with multiplatform database drivers
- Koin / Kodein- Multiplatform-friendly dependency injection frameworks
- Compose Multiplatform- JetBrains UI framework sharing UI code across Android, iOS, desktop, and web
Hierarchical Source Sets (Default Target Hierarchy)
Sharing code across related native targets without hand-wiring dependsOn for every source set.
kotlin { // Kotlin 1.9+ applies the default hierarchy template automatically, // creating intermediate source sets like `appleMain`/`nativeMain` // wherever two or more matching targets are declared androidTarget() iosX64() iosArm64() iosSimulatorArm64() macosX64() macosArm64() linuxX64() // appleMain (iosMain + macosMain) and nativeMain (appleMain + linuxMain) // are created for you; code shared by all Apple targets goes here sourceSets { val appleMain by getting { dependencies { implementation("io.ktor:ktor-client-darwin:2.3.11") } } val nativeMain by getting }}
expect/actual for typealias, object, and enum
expect/actual is not limited to classes and functions -- it covers most top-level declarations.
// commonMain: expect a typealias mapping to a platform-native typeexpect class AtomicCounter(initial: Int) { fun incrementAndGet(): Int}// commonMain: expect object for a platform-specific singletonexpect object Logger { fun log(message: String)}// commonMain: expect enum, actual enum must declare the same entriesexpect enum class Theme { LIGHT, DARK, SYSTEM }// androidMainactual typealias AtomicCounter = java.util.concurrent.atomic.AtomicIntegeractual object Logger { actual fun log(message: String) = android.util.Log.d("App", message)}actual enum class Theme { LIGHT, DARK, SYSTEM }
Advanced KMP Gotchas
Issues that surface once a KMP project grows past a toy example.
- expect fun cannot have a default value- Default parameter values must live on the `actual` side only, or in a non-expect wrapper in commonMain
- iosMain granularity- iosX64/iosArm64/iosSimulatorArm64 are separate source sets; put shared iOS-only code in the `iosMain` intermediate set, not per-arch
- kotlinx.coroutines Dispatchers.Main- Requires the `kotlinx-coroutines-core` multiplatform artifact plus the platform's UI dispatcher; iOS needs `kotlinx-coroutines-core` >= 1.6 with the new memory model
- Static frameworks for CocoaPods- Set `isStatic = true` on the iOS framework when embedding via CocoaPods to avoid duplicate-symbol linker errors
- expect/actual classes are still experimental-flagged- `expect`/`actual` classes (not functions/properties) require `-Xexpect-actual-classes` or an opt-in annotation on older toolchains
- Gradle configuration cache- Native compilation tasks (K/N) can be slow to reconfigure; enable Gradle's configuration cache to avoid re-resolving Kotlin/Native toolchains every build
cinterop: Calling a C Library from Kotlin/Native
Binding a native C API into iosMain via a .def file and cinterop configuration.
# src/nativeInterop/cinterop/zlib.defheaders = zlib.hheaderFilter = zlib.hlinkerOpts = -lz# build.gradle.ktskotlin { iosArm64 { compilations.getByName("main") { cinterops { val zlib by creating { defFile(project.file("src/nativeInterop/cinterop/zlib.def")) } } } }}# Usage in iosArm64Main:# import zlib# val level = Z_BEST_COMPRESSION
Multiplatform Testing with kotlin.test
Writing one test suite in commonTest that runs against every registered target.
// commonTest/kotlin/UserRepositoryTest.ktimport kotlin.test.Testimport kotlin.test.assertEqualsclass UserRepositoryTest { @Test fun `formats display name correctly`() { val repo = UserRepository(FakeApiClient()) assertEquals("Ana D.", repo.displayName("Ana", "Diaz")) }}// Platform-specific behavior can still be tested with expect/actual test// utilities, e.g. an `expect fun runBlockingTest(block: suspend () -> Unit)`// implemented differently per target when a common coroutine test API// isn't available on every platform yet.
Keep expect/actual declarations as thin as possible — put as much logic as you can in commonMain and only push the unavoidable platform-specific bits (file I/O, native APIs) behind the expect/actual boundary, so most of your codebase stays testable once instead of per-platform.