Flutter or Kotlin Multiplatform? Choose by What You Share
The honest comparison: Flutter shares the interface and the logic through its own renderer, Kotlin Multiplatform shares the logic and leaves each platform its native interface. Which costs less depends on what you already have.
The question is usually asked as "which is better", and answered with a feature list. That is not how the decision is actually made. Both ship real apps to millions of users. The difference is what each one shares, and therefore what you still have to write twice.
The one-sentence difference
Flutter draws its own interface with its own rendering engine, so one codebase produces the screens on both platforms. Kotlin Multiplatform shares the parts under the interface — models, networking, storage, business rules — and leaves the screens to Jetpack Compose on Android and SwiftUI on iOS.
Everything else follows from that.
Start from what you already have
A brownfield native app. If you have a working Android app and a working iOS app, Kotlin Multiplatform fits the way you work. You can move one module — the API client, say, or the pricing rules — into shared code and ship it, without a rewrite and without a flag day. Flutter in an existing app is possible, but adding a second rendering engine and a second toolchain to an app that already builds is a large change for a small first win.
A new product with a small team. Flutter is hard to beat here. One codebase, one language, one set of widgets, one build. A team of three can cover both platforms, and the screens look identical because they are the same screens.
A design system that must feel native. If your app lives or dies by feeling like the platform — system controls, platform navigation gestures, accessibility behaviour that matches the OS exactly — native interfaces with shared logic is the safer road. Flutter can get very close, and the gap keeps narrowing, but you are reproducing platform behaviour rather than inheriting it.
What each one costs you
Flutter
- Anything the framework does not wrap needs a plugin or a platform channel, which means writing Kotlin and Swift anyway — so budget for a developer who can.
- Your app carries the engine. The size difference matters for some markets and not at all for others; measure it against your own install numbers rather than arguing about it.
- Your team writes Dart. That is easy to learn and harder to hire for than Kotlin or Swift in most cities.
Kotlin Multiplatform
- You still build two interfaces. Shared logic typically covers 30 to 60 per cent of the code, not 90.
- The iOS side needs someone who is comfortable in Xcode and Swift, and who will not be surprised when a Kotlin flow has to be bridged to something SwiftUI can observe.
- Tooling has improved enormously but is still younger than either native stack on its own; expect to read release notes.
A table you can argue with
| If this is true | Lean towards |
|---|---|
| Two native apps already exist and work | Kotlin Multiplatform |
| New product, small team, speed matters most | Flutter |
| Interface must match platform conventions exactly | Kotlin Multiplatform |
| Design is custom and identical on both platforms | Flutter |
| Heavy use of new OS features on day one | Kotlin Multiplatform |
| Team already writes Kotlin and no Swift | Either, but hire for iOS before KMP |
| Complex business rules that must not differ by platform | Kotlin Multiplatform |
What moving one module actually looks like
The Kotlin Multiplatform path is easier to picture with code. Say both apps compute the fee for a course, and the two implementations have drifted — Android applies the discount before tax, iOS after. Move the rule, and only the rule:
// shared/src/commonMain/kotlin/pricing/Fee.kt
data class Fee(val listPrice: Int, val discountPercent: Int) {
val payable: Int get() = listPrice - (listPrice * discountPercent / 100)
}
Android uses it as ordinary Kotlin. iOS gets a framework and uses it from Swift:
let fee = Fee(listPrice: 25000, discountPercent: 68)
priceLabel.text = formatter.string(for: fee.payable)
Nothing else changed: both apps keep their own screens, their own navigation and their own release process. That is why the brownfield case is so much stronger for KMP than for Flutter — the first shared module can be a week of work, and if it does not suit you, deleting it is also a week.
The friction appears at the boundary. Kotlin's Flow and suspending functions do not map onto Swift concurrency on their own; teams either wrap them at the edge or use a library that does. Budget for that bridge once, early, rather than discovering it in every feature.
The test that settles it
Pick the hardest screen in your product — the one with the payment flow, the offline cache, or the camera — and build it twice in a week, once each way. Not a to-do list demo: the hard screen. You will learn more about which choice fits your team than any comparison article can tell you, including this one.
Whichever you choose, write the business rules so they do not depend on the framework. Teams that keep their domain logic free of UI types find the second decision, years later, much cheaper than the first one was.