Chapter 1 — Why Kotlin Multiplatform, Honestly
Where you are in the book. This is the chapter that decides whether the rest of the book is worth your time. You won't write a line of code here. Instead, you'll come away with an honest mental model of what Kotlin Multiplatform is, what it isn't, where it genuinely earns its keep, and where it will let you down. Everything we build later rests on the decisions we reason through in these pages.
The problem you already know
If you've shipped a mobile product of any real size, you've lived this story.
Someone writes a function that decides whether a user's free trial has expired. It checks the purchase date, the current time, the grace period, the promo extensions, and the edge case where the clock on the device is wrong. It's maybe forty lines of careful logic, and it took an afternoon to get right because the edge cases were nasty.
Then it gets written a second time. In Swift. By a different person. Who finds two of the edge cases but not the third. Six months later a user in a timezone nobody tested gets locked out of a trial they paid for, and the bug exists on exactly one platform, because the two implementations quietly drifted apart the moment they were born.
This is the double-work problem, and it is the single most expensive, least glamorous tax in cross-platform mobile development. It isn't really about typing the code twice — typing is cheap. It's about maintaining two sources of truth for the same idea, forever, and paying for every place they fall out of sync with bugs that appear on one platform and not the other.
Every cross-platform technology that has ever existed is, at bottom, an answer to this problem. Flutter answers it by replacing both platforms with its own engine. React Native answers it with a JavaScript bridge. Kotlin Multiplatform answers it differently from either of them, and that difference is the whole reason this book exists. So before we talk about source sets and Gradle and expect/actual, let's be precise about what KMP actually is — because most of the confusion in this space comes from people assuming it's something it isn't.
What Kotlin Multiplatform actually is
Here is the cleanest one-sentence definition I can give you:
Kotlin Multiplatform lets you write Kotlin once and compile it to genuinely native binaries for multiple platforms, so you can share the parts of your app that have no reason to be different.
Read that sentence again, because every word in it is doing work.
"Compile to genuinely native binaries." This is the part people miss. KMP is not a runtime. There is no JavaScript bridge, no embedded interpreter, no virtual machine shipped inside your app. Your shared Kotlin compiles down to a real .jar/.class for Android (it is the JVM, the same as always) and, through Kotlin/Native and LLVM, to a real native framework for iOS — the same kind of compiled artifact Swift produces. When your iOS app calls into your shared code, it isn't crossing a bridge. It's calling a native function. This is why KMP doesn't carry the performance penalty that bridged or interpreted frameworks do: there's nothing to bridge.
"The parts of your app that have no reason to be different." This is the philosophical core. A trial-expiry calculation has no reason to behave differently on iOS than on Android — the rules are the rules. Your networking, your data models, your validation, your business logic, your database access: none of these have a legitimate reason to differ by platform. That's the shared code. Meanwhile, the way a button feels under your thumb, the way a list scrolls, the way the back gesture works — those absolutely can have reasons to differ, and KMP's design respects that.
So the mental model is not "write your app once." It's "stop writing the boring, identical 60% of your app twice." That framing will save you a lot of grief, because people who expect "write once, run everywhere" come away disappointed, and people who expect "share the logic, keep the platforms" come away delighted. Set your expectations with the second group.
KMP is not a UI framework
This is the most important sentence in the chapter, so it gets its own heading.
Kotlin Multiplatform, by itself, does not draw a single pixel. It shares logic. The moment you want to share UI — actual screens, buttons, layouts — you are reaching for a separate, optional technology that happens to be built on top of KMP. That technology is Compose Multiplatform, and conflating the two is the number one source of confusion for newcomers.
Let's clear that up now.
What Compose Multiplatform is, and why it's a separate decision
If you've written Android in the last few years, you know Jetpack Compose: the declarative UI toolkit where you describe what the screen should look like as a function of state, and the framework figures out the rest. You write @Composable functions, you hoist state, you let recomposition do the work.
Compose Multiplatform (CMP) is JetBrains' project to run that exact programming model — the same @Composable functions, the same state model, the same mental framework — on iOS, desktop, and web, not just Android. You write a screen once as Compose, and it renders on an iPhone.
So now we have two distinct things, and you need to hold them separately in your head for the rest of this book:
| Kotlin Multiplatform (KMP) | Compose Multiplatform (CMP) | |
|---|---|---|
| What it shares | Logic: networking, database, business rules, view models | UI: screens, components, navigation, theming |
| What it is | The core code-sharing technology | A UI toolkit built on top of KMP |
| Is it optional? | It's the foundation — you always have it | Yes — entirely optional |
| The alternative | (none — it's the base layer) | Write native UI: Jetpack Compose on Android, SwiftUI on iOS |
The key insight: you can use KMP without CMP. Plenty of serious production apps — including some at companies you've heard of — share their entire logic layer with KMP and then build the UI twice, natively: Jetpack Compose for Android, SwiftUI for iOS. They get the consistency benefit where it matters most (the logic) and pay for two UIs deliberately, because they want every screen to feel perfectly native and they have the team to do it.
That "shared logic, native UI" approach is a completely legitimate, arguably conservative way to adopt KMP, and for years it was the default recommendation. This book takes the more ambitious path — shared logic and shared UI with Compose Multiplatform — because that's where the technology is heading, it's now stable enough to bet on, and frankly it's the more interesting story to teach. But I want you to understand from page one that this is a choice, that the choice has trade-offs, and that we'll keep the native escape hatch open the entire way through. When sharing the UI is the wrong call for a particular screen, I'll tell you, and I'll show you how to drop down to native for that screen specifically.
This brings us to the single most useful way to think about KMP adoption.
The sharing spectrum
KMP is not an all-or-nothing decision, and that is its most underrated feature. Picture a dial.
Share nothing Share everything
│ │
▼ ▼
Two fully Share Share logic Share logic One shared
native apps a model (Ktor, Room, + most UI codebase,
(Swift + or utility Koin) → native (CMP) → thin thin native
Kotlin) here & there UI per platform native shells entry points
▲ ▲
(conservative (this book's
production default)
default for years)
Every team that adopts KMP lands somewhere on this dial, and — crucially — you can start on the left and move right over time. You can take an existing, shipping, native Android app, extract one well-isolated piece of logic into a shared module, compile it into your iOS app, and ship. No rewrite. No big bang. No betting the company. If it goes well, you extract another piece next quarter.
This incremental adoption model is the reason KMP keeps showing up in places where Flutter and React Native never could: large, mature, native codebases that can't and won't be rewritten, but that can absorb a shared module here and there until, gradually, the expensive-to-maintain logic lives in one place. We'll build our app greenfield, sitting near the right end of the dial, but I'll point out repeatedly how the same patterns let an existing app slide rightward one step at a time. (Appendix B is devoted entirely to that migration story, because many of you will arrive at this book with an app, not a blank slate.)
How KMP differs from Flutter and React Native
You're going to get asked this in an architecture meeting, so let's make sure you can answer it well.
The other two big cross-platform frameworks share a philosophy that KMP rejects. Flutter and React Native both, in their own ways, try to replace the native platform. Flutter brings its own rendering engine and paints every pixel itself with Skia; your "button" is not a real iOS button, it's Flutter's drawing of one. React Native runs your logic in JavaScript and reaches across a bridge to manipulate native views. In both cases, the framework sits between your code and the platform, and owns the relationship.
KMP's philosophy is the opposite: augment the native platform, don't replace it. Your shared Kotlin compiles to a native library that your perfectly ordinary native app links against. On Android, it's just more JVM code in your normal Android project. On iOS, it's a native framework your Xcode project imports like any other. There is no engine in the middle. You retain 100% access to every native API, because you never left the native platform — you just brought some shared logic along for the ride.
This leads to a trade-off you should internalize:
- Flutter/React Native optimize for UI development speed and a single unified app. You move fast, especially early, and you get one codebase. The cost is that you're living inside the framework's world, and when you need something the framework didn't anticipate, you're writing platform channels and fighting the abstraction.
- KMP optimizes for native integration and incremental, low-risk adoption. You keep full native power and you can adopt gradually inside an existing app. The cost is more ceremony — two platform entry points, more setup, and a build system that asks more of you.
Neither is "better." They're answers to different questions. If you're a small team building a brand-new app from scratch and you want maximum UI velocity, Flutter is a genuinely strong choice and I won't pretend otherwise. If you have native expertise, an existing app, a need for deep platform integration, or a strong preference for keeping your UI truly native where it counts — KMP fits the shape of your problem better. Knowing which problem you have is more valuable than knowing which framework is fashionable.
Where KMP genuinely wins
Let me be specific, because vague enthusiasm helps no one.
You eliminate the drift bug class entirely for shared code. That trial-expiry bug from the opening can no longer exist on one platform and not the other, because there is only one implementation. An entire category of "works on Android, broken on iOS" tickets simply stops being generated. For logic-heavy apps, this is the whole ballgame.
You keep native performance and native API access. Because there's no bridge and no engine, your shared code runs at native speed, and your native code can do anything the platform allows. You are never locked out of a new iOS API because your framework hasn't wrapped it yet.
Adoption is incremental and reversible. You can introduce KMP into an existing app one module at a time, prove it works, and expand — or stop. Compare that to "rewrite the app in Flutter," which is a one-way door.
For Android developers specifically, the on-ramp is unusually gentle — and this deserves its own section, because it's the reason this book is aimed at you.
Why Android developers have an unfair advantage right now
Everything in KMP is Kotlin. You already write Kotlin. You already know coroutines, Flow, sealed classes, data classes, and the standard library. None of that is new.
But it goes deeper than the language, and this is the part that's genuinely new as of recently. Google has been making its own Jetpack libraries — the ones you already use every day — multiplatform. The data persistence library you reach for, Room, now runs on iOS through KMP. DataStore runs on iOS. ViewModel and the lifecycle components run in shared code. So when you learn KMP today, you are not throwing away your Android knowledge and learning a foreign stack. You're taking the exact tools you already know — Room, ViewModel, Compose — and discovering they now run on the other platform too.
The networking library we'll use, Ktor, is JetBrains' own and was multiplatform from the start. The dependency injection library, Koin, is multiplatform. The whole modern KMP stack is, from an Android developer's seat, startlingly familiar. Much of this book will feel less like learning a new framework and more like discovering that the framework you already know quietly grew the ability to run on iPhones.
This is a real, time-sensitive advantage. The skill of building production KMP apps is still relatively scarce — the people who are comfortable in both the shared layer and the platform-specific layer aren't on every street corner yet — while demand for it has roughly doubled in the span of a year. You are learning a valuable, in-demand skill at the moment it's transitioning from "early adopters" to "mainstream," with a head start that iOS-only developers simply don't have. That's a good place to be standing.
Where KMP will let you down (read this part twice)
A book that only tells you the good parts is marketing, not teaching. So here, plainly, are the things that will frustrate you. None of them are dealbreakers for the app we're building, but you should walk in with clear eyes.
iOS build times can hurt. Compiling Kotlin to native for iOS through Kotlin/Native is slower than you're used to on Android, especially on a cold build. It's improved a lot and keeps improving, but the first time you wait for a clean iOS build you'll feel it. We'll talk about mitigations, but I won't pretend it's instant.
The iOS binary is bigger. Sharing UI with Compose Multiplatform adds a meaningful chunk to your iOS app size — think single-digit megabytes of overhead for the Compose runtime and rendering. For most apps this is a non-issue. If you're shipping something where every megabyte is fought over, weigh it deliberately.
Kotlin-to-Swift interop is good but not yet seamless. When your shared Kotlin is consumed from Swift, it has historically gone through an Objective-C bridge, which can make the generated API feel slightly un-Swifty. A direct Swift export is actively being built but isn't fully mature, and in the meantime the community standard is an excellent tool called SKIE that smooths the rough edges — particularly around how Kotlin's suspend functions and Flow appear to Swift's async/await. We have an entire chapter on this (Chapter 9) because it's the seam where the two worlds meet, and seams are where things leak.
Some things still genuinely need native code. Maps, certain sensors, deep platform integrations, and anything brand-new on a platform will still require you to write a bit of Android and a bit of iOS. KMP's honesty is that it gives you a clean, first-class way to do this (expect/actual and platform abstractions, which we'll cover in depth) rather than pretending the need doesn't exist. But if your fantasy is never touching Swift or the Android SDK again, KMP will puncture it. That's a feature, not a bug — but it's a real expectation to reset.
Compose Multiplatform on iOS is stable, but it isn't SwiftUI. It renders beautifully and performs well, with native-feeling scroll physics, text selection, and gestures. But it is Compose's rendering of an iOS-feeling UI, not literally SwiftUI components. For the vast majority of screens this is completely fine and your users will never know. For a handful of screens that need to feel exactly, pixel-and-physics-perfectly native, you'll drop down to SwiftUI — which, again, KMP lets you do cleanly. Know which screens those are before you start.
If you read that list and thought "those are reasonable trade-offs for killing the double-work problem," congratulations — you have the right temperament for this technology. If you read it and thought "I never want to see Xcode again," KMP may frustrate you, and it's better to know that now, on page 12, than three chapters in.
Why this is a good moment, specifically
It's worth being concrete about timing, because KMP's reputation is lagging behind its reality. For years the honest answer to "is it production-ready?" was "for shared logic, yes; for shared UI, not quite." That answer changed.
Compose Multiplatform for iOS reached stable status in 2025 — that's the milestone that took shared UI from "brave early-adopter territory" to "a normal engineering decision." Google's Jetpack libraries went multiplatform, so the tools you already know now run on iOS. JetBrains and Google are both actively invested, with published roadmaps. And the companies running KMP in production at scale aren't experiments — they include names like Netflix, McDonald's, and Cash App, the last of which has been running KMP in production since before it was cool.
So the question this book answers is no longer "can you build a real app with KMP?" That's settled. The question is "how do you build a real, production-grade app with KMP — properly, with all the unglamorous parts that separate a tutorial from a shippable product?" That's the gap we're here to fill.
The app we'll build: Atlas
A book that teaches each technology in isolation — here's a networking tutorial, here's a database tutorial — produces developers who know fifteen things and can't assemble them into anything. That's not this book. We're going to build one real app, from an empty project to a shippable product, and every technology will enter the story at the moment the app actually needs it. You'll never add a library because the syllabus said so; you'll add it because the app you're holding has a problem only that library solves.
Our app is Atlas: a places-and-routes explorer.
The premise is simple and the feature set is chosen deliberately. With Atlas, you:
- Discover places near you — restaurants, parks, points of interest — pulled from a remote service. (This is why the app needs networking. Enter Ktor.)
- Save the places you care about so they're available offline, even on a plane or a subway. (This is why the app needs a local database. Enter Room KMP, and an offline-first architecture.)
- See those places on a map, with markers and detail. (This is why the app needs native map integration — and an honest lesson in platform interop.)
- Track your location and walking route as you explore, turning "places near me" into something live. (This is why the app needs location and sensors, behind a unified cross-platform abstraction.)
- And the whole thing is production-hardened — wired together with dependency injection, observable through logging and analytics, resilient to failure, tested, and shipped to both stores through CI. (This is why the app needs everything in Part V — the parts most tutorials skip and most real products live or die by.)
Here's the design principle behind Atlas, and it's the thing that makes this book different from a pile of tutorials: every technology on our list has an honest, app-driven reason to exist. I didn't pick "an app that uses Ktor, Room, Koin, maps, and sensors." I picked an app that genuinely needs networking, persistence, dependency management, maps, and sensors — and then let the technologies fall out of the requirements. When we add Room, it's because you just got frustrated that your saved places vanish when you close the app. When we add a logging abstraction, it's because you just spent twenty minutes debugging something that a log line would have caught in two. The motivation always comes first. The implementation answers a need you can feel.
Atlas is also chosen to be just realistic enough. It touches every major area of production mobile development — remote data, local data, platform capabilities, UI, observability, and shipping — without sprawling into something so large you lose the thread. It's the smallest app I could design that still forces every important lesson to come up naturally.
How this book works
A few promises about the shape of what's ahead, so you know what you're signing up for.
Concept first, then implementation. Always. Every chapter explains the idea before touching the keyboard. You'll understand why an offline-first repository is structured the way it is before you write one, why expect/actual exists before you use it, why dependency injection earns its complexity before you wire up Koin. Code without understanding is something you copy and then can't debug. We're after the understanding; the code is how we make it concrete.
One app, built incrementally. We are not going to bounce between fifteen unrelated sample projects. We build Atlas, and each chapter leaves it more complete than it found it. By the end, you won't have a folder of disconnected exercises — you'll have a single, coherent, shippable application whose every line you understand because you watched it arrive.
The hard parts get the most honesty. When something is genuinely awkward — iOS interop, the Swift seam, native maps inside shared UI — I'm going to slow down and tell you the truth, including the parts that are annoying. The chapters most tutorials gloss over are the ones I've weighted most heavily, because they're where real projects get stuck.
What you'll need. A computer that can run Android Studio, and — this is the non-negotiable part for the iOS half — a Mac with Xcode. This is not a limitation of the book; it's a limitation of Apple's universe. You cannot build or run an iOS app without macOS and Xcode, and no cross-platform technology, KMP included, changes that. If you only have access to a Windows or Linux machine, you can still follow the entire Android side and learn the vast majority of the concepts — the shared code is identical — but you won't be able to run the iOS app. I'll flag the Mac-only moments clearly as we reach them. The next chapter sets up the tooling properly; this one only needs your attention.
Familiarity assumed. I'm assuming you can write Kotlin and have built at least a small Android app — you know what an Activity is, you've seen Jetpack Compose, coroutines don't scare you. I'm assuming you know nothing about KMP, iOS, or Swift, and I'll teach the iOS-side concepts as we need them. You do not need to be an iOS developer. By the end, you'll be a more capable one than you expected.
What you'll have by the end
When you close this book, you will have built Atlas: a single Kotlin Multiplatform application, sharing both its logic and its UI across Android and iOS, that talks to a network, persists data offline, shows native maps, reads device sensors and location, manages its dependencies cleanly, logs and reports on itself in production, handles failure gracefully, is covered by tests, and ships to both app stores through an automated pipeline.
More importantly, you'll understand why every piece is the way it is — which means you'll be able to build the next app, the one that isn't Atlas, on your own. That's the actual goal. Atlas is the vehicle. Understanding is the destination.
And you'll be able to answer, from experience rather than hype, the question your team lead is going to ask you next month: "Should we be using this?" — with a real answer, including the honest caveats, because you'll have felt both the wins and the friction with your own hands.
Summary
Let's consolidate the mental model before we touch any tooling.
- Kotlin Multiplatform (KMP) lets you write Kotlin once and compile it to genuinely native binaries for multiple platforms, so you can share the logic that has no reason to differ by platform. It is not a runtime, not a bridge, and not a UI framework — it augments the native platforms rather than replacing them.
- Compose Multiplatform (CMP) is a separate, optional UI toolkit built on top of KMP that runs the Jetpack Compose programming model on iOS, desktop, and web. Sharing UI is a distinct decision from sharing logic, with its own trade-offs.
- KMP adoption lives on a spectrum, from sharing one small utility to sharing nearly everything, and you can move along that spectrum incrementally and reversibly — which is why it slots into existing native apps that could never be rewritten wholesale.
- KMP wins on eliminating cross-platform drift bugs, native performance and API access, incremental adoption, and — for Android developers specifically — an unusually gentle on-ramp, since the tools you already know (Room, ViewModel, Compose, Kotlin itself) now run on iOS too.
- KMP costs you slower iOS builds, a larger iOS binary, a not-yet-seamless Kotlin-to-Swift seam, the occasional unavoidable bit of native code, and a Compose-rendered (not literally SwiftUI) iOS UI. These are reasonable trade-offs for the right app — and they're trade-offs, not dealbreakers.
- The timing is good: Compose Multiplatform for iOS is stable, Google's Jetpack libraries are multiplatform, both JetBrains and Google are invested, and serious companies run KMP in production at scale.
- We'll learn all of this by building Atlas, one real places-and-routes app, where every technology earns its place by solving a problem the app genuinely has.
In the next chapter, we stop talking philosophy and start understanding how KMP actually works under the hood — source sets, compile targets, and the expect/actual mechanism that makes "write once, adapt per platform" possible. That's the machinery everything else in the book is built on. Let's get into it.
End of Chapter 1.