Kotlin Multiplatform: expect, actual and shared modules
Structure a KMP project, declare expect and actual, choose multiplatform libraries, and share domain and networking code across Android and iOS.
Source sets and targets
// shared/build.gradle.kts
plugins {
kotlin("multiplatform") version "2.1.0"
kotlin("plugin.serialization") version "2.1.0"
}
kotlin {
androidTarget()
iosX64(); iosArm64(); iosSimulatorArm64()
jvm()
sourceSets {
commonMain.dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.7.3")
implementation("io.ktor:ktor-client-core:3.0.1")
implementation("io.ktor:ktor-client-content-negotiation:3.0.1")
}
androidMain.dependencies { implementation("io.ktor:ktor-client-okhttp:3.0.1") }
iosMain.dependencies { implementation("io.ktor:ktor-client-darwin:3.0.1") }
commonTest.dependencies { implementation(kotlin("test")) }
}
}| Source set | Compiled for | Can use |
|---|---|---|
commonMain | Every target | Only common APIs and libraries |
androidMain | Android | Android SDK plus common |
iosMain | All iOS targets | Apple platform APIs plus common |
commonTest | Every target | Common test utilities |
Put as much as possible in commonMain. Every platform-specific API you add there forces an expect declaration and at least two implementations to maintain.
expect and actual
// commonMain
expect class PlatformClock() {
fun nowMillis(): Long
val name: String
}
expect fun currentPlatform(): String
// androidMain
actual class PlatformClock actual constructor() {
actual fun nowMillis(): Long = System.currentTimeMillis()
actual val name: String = "android"
}
actual fun currentPlatform(): String = "Android " + android.os.Build.VERSION.SDK_INT
// iosMain
actual class PlatformClock actual constructor() {
actual fun nowMillis(): Long = (kotlin.system.getTimeMillis())
actual val name: String = "ios"
}
actual fun currentPlatform(): String = "iOS " + platform.UIKit.UIDevice.currentDevice.systemVersion
// never leak a thread-local assumption into common code:
// expect/actual is the seam, not a branch on platform inside common logic⚠️
An
expect declaration is a compile-time contract: every target must provide an actual, or the build fails for that target only. Run the full target build locally before adding one, not just the Android variant you usually compile.What is worth sharing
// shared/src/commonMain/kotlin/com/example/domain/ArticleRepository.kt
class ArticleRepository(
private val api: ArticleApi,
private val cache: ArticleCache,
) {
suspend fun recent(): List<Article> =
runCatching { api.recent() }
.onSuccess { cache.save(it) }
.getOrElse { cache.load() ?: emptyList() }
}
// iOS consumes the shared module as a framework and calls into it from Swift
// val repository = ArticleRepository(api: KtorArticleApi(), cache: InMemoryCache())
// repository.recent { articles, error in ... } // exported as a completion handler- Share the domain, the data layer and validation. Those are where duplication costs the most and platform APIs are rarely needed.
- Keep the UI native unless you have a strong reason otherwise: SwiftUI and Compose are both excellent, and a shared abstraction over them ages badly.
- Export a small, friendly API from the shared module. Every public type becomes part of the contract your Swift and Kotlin callers compile against.
- Use a callback or a Flow bridge for async APIs, and test the bridge — that is where the two platforms most often disagree.
FAQ
Is Kotlin Multiplatform the same as Compose Multiplatform?
No. KMP shares logic and produces a framework or library for each platform; Compose Multiplatform shares the user interface as well. Sharing logic is low risk and usually the first step.
How much code can realistically be shared?
For a typical app, expect 40 to 70 percent: models, networking, storage, validation, formatting and business rules. The remaining UI and platform integration stays native and is usually the more pleasant part to keep that way.
Related
JSON, serialization and HTTP clients Java interop, JVM tooling and build performance
Last refreshed 2026-09-18.