I shipped a small dev-news reader to Google Play with the entire client written
in one Compose Multiplatform codebase — every screen in commonMain, no
per-platform UI. This is an honest field report on what that actually costs in
production: the numbers, the native seams, and the parts that still hurt (hi,
iOS). The repo is open source (MIT), so everything here is checkable.
TL;DR — For a content/list/detail app, CMP is comfortably production-ready
on Android. 96.9% of the shared module iscommonMain; the native cost is
concentrated in ~10expect/actualseams. iOS compiles and renders, but isn't
polished yet.
The numbers
| Source set | Files | Lines | Share |
|---|---|---|---|
| commonMain | 59 | ~7,700 | 96.9% |
| androidMain | 2 | 123 | 1.6% |
| iosMain | 3 | 127 | 1.6% |
All 17 screens live in commonMain — trending list, aggregated feed, README
detail, profile with paging, settings, favorites — and there isn't a single
if (isAndroid) branch in the UI.
The 3% that isn't shared
The native cost isn't spread thinly across the codebase. It's concentrated in
~10 expect/actual seams, and this is the entire list:
- Platform info — app version, system language, User-Agent string
- System interaction — open URL, open app settings, share sheet
-
Analytics — a
trackEventhook (Android → Aptabase; iOS is deliberately a no-op for now) - WebView — the messy one (below)
Everything that touches a platform API is small and enumerable. Everything else
came for free.
Why expect/actual and not an interface + DI?
For these ~10 seams, expect/actual was the least ceremony: no DI wiring, and
the compiler refuses to build until every target implements the declaration. The
moment a seam has more than one implementation, or I'd want to fake it in tests,
an interface in commonMain with injected impls is the better tool. For a fixed
set of platform primitives, expect/actual wins on friction.
The ugliest boundary: WebView
I have two WebView paths, and I'll be precise because the repo is open:
-
Rendering GitHub READMEs from an HTML string — inline
expect/actualoverandroid.webkit.WebView/WKWebView, JS disabled, theming done by injecting CSS colors so light/dark stays consistent. ~30–50 lines per side. - Opening external links in-app — this one I pulled into a standalone library, kmp-webview, because the lifecycle/config surface (file chooser, media capture, title handling) got hairy enough to deserve its own home.
WebView is hands-down the least elegant part of the codebase. If you've wrapped
WKWebView / Android WebView behind a common composable, I'd genuinely welcome
a sanity check on the library's API.
Honest status: iOS is WIP
Android is live on Google Play. iOS compiles and every Compose screen renders in
the simulator — but it isn't polished:
- iPad share crashes without a
popoverPresentationControlleranchor (there's a literal TODO) - iOS has no analytics wired
- the AI-chat screen is Android-only right now
Not vaporware, just not shipped. And honestly, iOS is where the KMP tax
actually shows up — not in the shared code, but in the toolchain around it:
living with Xcode for the shell, slower clean builds when the iOS target is in
the loop, and fewer battle-tested libraries. The Kotlin/Compose side is smooth;
the iOS edge is where I spend my "fighting the tools" time.
Stack
Kotlin 2.3.21 · Compose Multiplatform 1.11.1 · Ktor 3.5.0 · Coil 3 ·
multiplatform-settings · Material 3. Navigation is a small stack-based setup in
commonMain (data objects/classes as route keys), no platform-specific nav.
Takeaways
- For a content/list/detail app, shared-UI CMP pays off on Android. Write 17 screens once, not twice.
-
The native tax is small and predictable. It lives at ~10
expect/actualseams — anything that wraps a platform view or calls a platform API. - The pain is exactly where you'd guess: WebView, and the iOS toolchain.
- expect/actual for fixed platform primitives; interfaces for anything you'd test or swap.
- Be honest about iOS. Compiling and rendering in a simulator is not the same as shipping.
The app is a GitHub Trending / Hacker News / Product Hunt reader, if you want
context on the screens. Repo (MIT): https://github.com/HarlonWang/TrendingAI —
and the WebView lib: https://github.com/HarlonWang/kmp-webview.
What's your experience taking CMP to production, especially on iOS? I'd love to
compare notes in the comments.

Top comments (1)
One thing I left out of the post to keep it tight: the biggest surprise wasn't
that the shared UI worked — it was how little ended up in the platform source
sets. I expected androidMain/iosMain to grow as the app did; instead they've
stayed almost flat at ~120 lines each while commonMain kept climbing.
A couple of things I'm genuinely unsure about and would love this community's
take on:
Happy to dig into the architecture or the numbers — ask away.