8 min read

Compose Multiplatform: The Reality Check

Sharing logic and sharing UI are different bets with different risk profiles. What Compose Multiplatform genuinely delivers, the platform-feel and organisational costs nobody puts in the evaluation doc, and the staged strategy that survives contact.

Kotlin MultiplatformCompose MultiplatformAndroidiOSArchitecture
HR

Hessam Rastegari

Senior Android Developer · 12 years shipping Android

TL;DR — Sharing logic (plain KMP) is a boring, low-risk, high-value decision. Sharing UI (Compose Multiplatform) is a much larger bet and most teams evaluate them as one question. CMP works and renders correctly on iOS; what "works" doesn't cover is platform feel (scroll physics, back-swipe, text selection, keyboard avoidance, VoiceOver), the organisational cost of iOS engineers debugging Kotlin they didn't write, and a UI ecosystem — maps, payments, camera — that stays platform-specific. Ship it for internal tools, desktop companions, brand-first products and Kotlin-first teams with no iOS org. Stage it everywhere else: share domain and data first, measure, then take one screen.

Two questions that get merged into one

"Should we share code across platforms?" and "Should we share the UI?" have different answers, and conflating them is the single most common mistake in this decision.

Sharing your domain model, your validation rules, your networking, your caching policy and your business logic is plain Kotlin Multiplatform. It's low-drama, it's where most of the genuine duplication lives — and duplicated business rules is where the two apps actually diverge in behaviour, which is the bug class that reaches users. There's a separate post in this series on what to share and what not to.

Sharing the UI is Compose Multiplatform, and it's a much bigger bet: bigger payoff, materially bigger risk, and a different set of people affected.

What genuinely works

Being fair to the technology, because the argument isn't "don't use it":

  • Android and desktop are production-ready and have been for a while. A desktop companion built from screens you already have is close to free.
  • iOS has matured a great deal. Rendering is correct, performance is good, interop with SwiftUI and UIKit exists in both directions, and real apps ship this way.
  • Web/Wasm is real and improving quickly, though it's the least settled target of the four.
  • One design system, one implementation. For a product whose visual identity overrides platform convention anyway, this is a genuine multiplier rather than a compromise.

Reality check 1: rendering was never the hard part

CMP draws your UI correctly on iOS. It's the thing that gets demoed, and it was never where the risk was.

The risk is the accumulated platform behaviour an iOS user has never consciously thought about and notices instantly when it's absent:

  • Scroll physics, deceleration curves and rubber-banding at the edges.
  • The interactive edge-swipe back gesture — and, more subtly, a partial swipe that snaps back.
  • Text selection: the magnifier, the handles, the callout menu.
  • Keyboard avoidance, and the way the scroll position settles when it dismisses.
  • Native-feeling context menus, share sheets and system dialogs.
  • VoiceOver conventions, which differ from TalkBack in ordering and phrasing.
  • Dynamic Type, Reduce Motion, and the rest of the iOS accessibility settings users actually enable.

Each of these is individually solvable, and the gap has narrowed a lot. The honest characterisation is that it's a long tail you are choosing to own, on a platform whose users are unusually sensitive to it. Budget for it as a permanent line item, not a migration task.

If your app's identity is its brand — a streaming service, a game-adjacent experience, a fintech product with a strong design system — much of this matters less, because you were already overriding platform convention. If your app is a utility that should feel like it came with the phone, it matters a lot.

Reality check 2: the expensive problem is organisational

This is the one that kills adoptions, and it never appears in the technical evaluation.

Your iOS engineers now debug Kotlin they didn't write. Stack traces cross a language boundary. The build is Gradle. The code review is in a language they didn't choose and a paradigm they may not prefer. Their platform expertise — the reason you hired them — is suddenly worth less on their own product.

If that team wasn't part of the decision, you don't have a technology problem, you have a staffing problem with a technology label. It surfaces six months later as attrition, or as an iOS app that nobody volunteers to maintain.

The teams where this goes well share a shape: Kotlin-first organisations, or teams small enough that the same people own both platforms anyway. The teams where it goes badly have a strong, established iOS practice that was told the plan rather than asked.

Reality check 3: the UI ecosystem stays platform-specific

Your logic dependencies are increasingly multiplatform — Ktor, coroutines, serialization, SQLDelight, Room's KMP support, and Coil for images.

Your UI dependencies mostly aren't. Maps, payment sheets, camera, in-app review, biometric prompts, ad SDKs, chat widgets, analytics session replay — anything that draws is platform-specific and, in many cases, also the piece you least want to reimplement.

So you write expect/actual for exactly the components you were hoping to share:

// commonMain
@Composable
expect fun PaymentSheet(
    amount: Money,
    onResult: (PaymentResult) -> Unit,
)

Two implementations, two SDKs, two sets of edge cases, plus a shared abstraction to maintain over the top. That's not an argument against CMP — it's a correction to the sharing percentage in the business case. Count the screens that touch a platform SDK before you promise a number.

Where I'd ship it without hesitating

  • Internal tools. Few users, all colleagues, no App Store review of the feel, and the alternative is usually "no iOS version". The best risk-adjusted use of CMP there is.
  • Desktop companions to an existing Android app — genuinely near-free.
  • Brand-first consumer products where the design system already overrides platform convention on both sides, so "doesn't feel native" was already the intended outcome.
  • Kotlin-first teams with no dedicated iOS organisation. If the real comparison is "an iOS app that feels 85% right" versus "no iOS app", that's an easy call, and it's the situation many product teams are actually in.
  • Prototypes and pre-product-market-fit apps, where shipping to both platforms this quarter beats polish you may throw away.

Where I wouldn't, today

A consumer app with an established iOS team and a high bar for platform feel. You'd spend a year re-implementing behaviour UIKit gives away, and you'd be asking the people best placed to notice the difference to maintain it.

Also: anything where the UI is the product's differentiation on iOS specifically, and anything with a hard constraint on binary size or cold start that you haven't measured with the Kotlin/Native runtime included. Measure that before promising it, not after.

The strategy that survives contact

Stage it. Each step has to earn the next.

  1. Share the domain and data layers. Plain KMP. Models, validation, networking, caching policy, business rules. Low risk, no UI debate, and the largest single reduction in "the two apps behave differently" bugs you will ever get.
  2. Measure. How much code actually moved? How many divergence bugs disappeared? Does the iOS team find the shared module tolerable to work with? These answers are data for step 3, and sometimes they end the conversation happily right here.
  3. Take one screen with CMP. Settings. Onboarding. A static informational flow. Something low-traffic, low-gesture and unglamorous — not the home feed, and not the checkout.
  4. Let the iOS engineers judge it, in a build on a real device, before anything else is committed. If they say it feels wrong, that is the finding, not an objection to be managed.

Most teams should stop after step 1 and be delighted. That isn't a failure of Compose Multiplatform — it's the correct answer for their shape, and it delivers most of the value that was ever on the table.

The rule

"We share 90% of our code" is a statistic. "Our iOS users can't tell" is the requirement. Share logic by default, because that's where duplication silently becomes divergent behaviour. Share UI deliberately, on a product whose feel you already control, with the people who own the platform in the room when you decide.

Keep reading