Kotlin Multiplatform — Share Logic, Keep Native Where It Matters
How one Kotlin codebase targets Android, iOS, desktop, and the web. A deep dive into source sets, the expect/actual mechanism, what is worth sharing, and how Compose Multiplatform now extends sharing all the way to the UI.
The same logic, written three times
Build a serious app today and you are likely building it more than once. An Android version in Kotlin, an iOS version in Swift, perhaps a web version in TypeScript. Each reimplements the same things: the networking, the data models, the validation rules, the business logic. Three teams, three bugs for every one mistake, three places to keep in sync. This duplication is one of the most expensive facts of modern app development.
Kotlin Multiplatform (KMP) attacks this with a specific philosophy, and it is worth contrasting it with the alternative you may know from the Dart/Flutter series. Flutter shares everything, including the UI, by shipping its own rendering engine and painting every pixel itself. KMP makes a different bet: share the logic — the parts that genuinely should behave identically everywhere — while letting each platform keep its native UI, native look, and native performance.
The result is that an iOS app built with KMP is still a real iOS app with SwiftUI on top; it just calls into shared Kotlin for its data and rules. You are not asking iOS developers to abandon their platform, only to stop rewriting the network layer. That pragmatism — share what should be shared, keep native what benefits from being native — is the same spirit that runs through the rest of Kotlin's design.
From experiment to stable
Multiplatform was a long time coming, and its timeline tells you how seriously JetBrains treats stability. The underlying technology — Kotlin/Native, which compiles Kotlin to native binaries via the LLVM toolchain (introduced in the Foundations episode) — was announced around 2017. Multiplatform itself spent years labelled "experimental" and then "alpha," while the team hardened the tooling and the compiler.
Kotlin Multiplatform was finally declared Stable in November 2023, with Kotlin 1.9.20. That five-plus-year incubation mirrors the patient arc of Kotlin itself — announced in 2011, 1.0 only in 2016. JetBrains would rather ship late and stable than early and shaky.
Two developments cemented KMP's momentum. First, Google embraced it: at Google I/O 2024, Google announced official support for Kotlin Multiplatform for sharing business logic across Android and iOS, echoing the Android-first endorsement Kotlin received back in 2017. Second, JetBrains shipped Compose Multiplatform, extending the Jetpack Compose UI framework beyond Android to desktop, iOS, and the web — so teams that do want to share UI can, while those who prefer native UI still can. KMP stopped being a promising experiment and became a mainstream architecture choice.
Source sets: common and platform code
A multiplatform project organises code into source sets. The most important is commonMain — code here is platform-agnostic Kotlin that compiles to every target. Alongside it sit platform-specific source sets like androidMain, iosMain, and jvmMain, which hold code that exists only for that platform.
When you build for Android, the compiler combines commonMain with androidMain and produces JVM bytecode packaged as an Android library. When you build for iOS, it combines commonMain with iosMain and produces, via Kotlin/Native, a native framework that an Xcode project can import like any other. The shared module is written once; each platform target compiles it together with its own platform source set.
This is why the compilation targets from the Foundations episode matter so much: KMP is those targets working in concert. The same commonMain source flows through Kotlin/JVM for Android, Kotlin/Native for iOS, and Kotlin/JS or Kotlin/Wasm for the web.
expect and actual
Common code sometimes needs something the platform must provide — the current time, a random-number source, secure storage, a device ID. The shared module cannot implement these itself, but it needs to refer to them. Kotlin's mechanism for this is the expect/actual pair, KMP's signature feature.
In commonMain you declare what you expect to exist, with no body. In each platform source set you provide the actual implementation.
// commonMain — the contract
expect fun currentTimeMillis(): Long
// androidMain — uses the JVM
actual fun currentTimeMillis(): Long = System.currentTimeMillis()
// iosMain — uses Apple's Foundation, via Kotlin/Native interop
actual fun currentTimeMillis(): Long =
(NSDate().timeIntervalSince1970 * 1000).toLong()
The compiler enforces the contract: if a target is missing an actual for some expect, the build fails. It works for functions, classes, properties, and more. In practice, modern KMP often prefers plain interfaces and dependency injection over expect/actual for larger abstractions, reserving the mechanism for small, leaf-level platform primitives — but it remains the conceptual heart of how shared code reaches platform capabilities.
Compose Multiplatform: sharing the UI too
For years, "share logic, keep native UI" was KMP's whole story. Compose Multiplatform, built by JetBrains on top of Google's Jetpack Compose, changed the menu by making shared UI an option rather than a requirement.
Jetpack Compose is Android's modern declarative UI toolkit — you describe the UI as functions of state (recall the lambdas-with-receiver and DSL ideas from the Functions episode; Column { Text("hi") } is exactly that pattern). Compose Multiplatform takes that same Kotlin UI code and renders it on desktop (JVM), iOS, and the web, in addition to Android.
// Shared UI — written once, runs on Android, iOS, desktop, web
@Composable
fun Greeting(name: String) {
Column {
Text("Hello, $name")
Button(onClick = { /* ... */ }) { Text("Tap me") }
}
}
This puts KMP and Flutter on more comparable footing for teams that want a single UI codebase — but with a crucial difference in heritage: Compose Multiplatform is Compose, the same framework Android developers already use, so an Android team's existing skills and UI carry straight over. The choice is now a spectrum: share only logic and write native UIs; or share logic and UI with Compose. Either way, the shared core is ordinary Kotlin — every concept from this series applies, unchanged, on every platform you ship to.
AI prompts — Kotlin Multiplatform
Use these prompts with an AI assistant to go deeper on the ideas in this episode.
Prompt 1 — what to share:
Here is my app's architecture across Android and iOS:
[describe your layers]
Which parts should move into a shared commonMain module, and which
should stay platform-specific? Explain the reasoning for each.
Prompt 2 — expect/actual design:
I need [current time / secure storage / device info / a UUID] in
shared code. Show me how to model it with expect/actual across
Android and iOS, and tell me whether an interface + DI would be
a better fit here.
Prompt 3 — choosing libraries:
For a KMP module that does networking, JSON, local storage, and
concurrency, which multiplatform libraries should I use and how do
they fit together? (Consider Ktor, kotlinx.serialization, SQLDelight,
kotlinx.coroutines.)
Prompt 4 — native UI vs Compose Multiplatform:
My team has existing native SwiftUI and Android Compose screens.
Walk me through the trade-offs of (a) sharing only logic with native
UIs vs (b) adopting Compose Multiplatform for shared UI, for our case:
[describe team and app]
Kotlin Multiplatform — Quiz
7 questions
Seven questions on KMP's philosophy, source sets, expect/actual, what to share, and Compose Multiplatform.