Cross-Platform Is a Spectrum, Not a Binary Choice
The question was never “native or cross-platform.” The question is: what are you actually sharing, and what does sharing cost you? Flutter and KMP answer that question differently — and for most production mobile teams, KMP’s answer is the more honest one.
The Fundamental Philosophical Difference
Flutter reimplements the UI layer from scratch using its own rendering engine ( Skia/Impeller). You share everything — business logic, UI, navigation — but what runs on device is Flutter’s widget tree, not platform components. The user experience is Flutter-on-iOS and Flutter-on-Android, not iOS or Android.
KMP takes the opposite approach: share only what doesn’t need to be native, keep native what does. Business logic, networking, data persistence, domain models — all shared Kotlin. UI is written natively: Jetpack Compose on Android, SwiftUI on iOS. Or you can go further with Compose Multiplatform if shared UI is genuinely valuable for your product.
This distinction has cascading consequences for every engineering decision downstream.
What KMP Actually Shares
interface AuthRepository { suspend fun login(email: String, password: String): Result<User> suspend fun refreshToken(): Result<String> suspend fun logout()}
// shared/src/commonMain/kotlin/com/myapp/data/AuthRepositoryImpl.ktclass AuthRepositoryImpl( private val apiClient: ApiClient, private val tokenStore: TokenStore,) : AuthRepository {
override suspend fun login(email: String, password: String): Result<User> = runCatching { val response = apiClient.post("/auth/login") { body = LoginRequest(email, password) } tokenStore.save(response.tokens) response.user.toDomain() }}This runs identically on Android and iOS. The iOS team consumes it through a generated Kotlin/Native framework:
// iOS — Swiftimport Shared
class LoginViewModel: ObservableObject { private let authRepository: AuthRepository
func login(email: String, password: String) async { let result = try? await authRepository.login(email: email, password: password) // Handle result with SwiftUI }}No FFI glue. No platform channels. No serialization boundary. A Kotlin interface, called directly from Swift.
Platform Fidelity Is Not a Soft Requirement
Flutter’s UI fidelity problem is well-documented but frequently dismissed. Here’s what it looks like in practice:
- Date pickers, bottom sheets, and navigation transitions that feel slightly off on iOS
- Text selection handles, context menus, and cursor behavior that diverge from UIKit conventions
- Accessibility integration (VoiceOver, TalkBack) that requires manual wiring instead of inheriting platform defaults
- Haptics, dynamic type, and system font scaling that need explicit handling for each platform idiom
For internal tooling or data-heavy dashboards, Flutter’s fidelity gap is irrelevant. For consumer apps where users have internalized platform expectations — banking, fintech, healthcare, e-commerce — it erodes perceived quality in ways that don’t show up in bug trackers but absolutely show up in App Store reviews.
KMP apps are iOS apps and Android apps. They inherit every platform behavior — scroll physics, system animations, accessibility, dynamic type — without ceremony.
Ecosystem and Tooling Depth
| Dimension | KMP | Flutter |
|---|---|---|
| Android tooling | Native — AS, Gradle, Profiler | Via plugin — functional but abstracted |
| iOS tooling | Native — Xcode, Instruments, TestFlight | Via flutter build — Xcode mostly hidden |
| Language on Android | Kotlin (first-class) | Dart (separate language to hire for) |
| Language on iOS | Swift (first-class) | Dart (Swift interop via method channels) |
| Existing Android codebase | Gradual adoption — no rewrite | Full rewrite or parallel app |
| > Existing iOS codebase | Gradual adoption — share logic incrementally | Full rewrite or parallel app |
| Hiring pool | Kotlin/Swift engineers (large) | Dart engineers (smaller, Flutter-specific) |
| Library ecosystem | Entire JVM + iOS ecosystems available | Flutter pub.dev — growing but narrower |
| Google support longevity | Backed by Google + JetBrains + AOSP | Backed by Google — single-vendor risk |
The hiring pool point is underrated. KMP lets you hire Kotlin engineers who become productive on the shared layer immediately, and iOS engineers who remain in SwiftUI. Flutter requires Dart fluency across the entire mobile team — a real recruiting constraint outside major metro areas.
The Gradual Adoption Advantage
This is KMP’s most underappreciated practical strength. You don’t adopt KMP by rewriting your apps. You adopt it incrementally:
- Extract one domain module — say,
UserPreferencesorAnalyticsTracker - Wire the shared Kotlin artifact into your existing Android and iOS apps
- Validate the approach works in your CI/CD, your team’s workflow, your release pipeline
- Expand the shared surface area over successive sprints
[versions]kotlin = "2.0.21"kmp-compose = "1.7.0"
[libraries]compose-multiplatform = { module = "org.jetbrains.compose:compose-gradle-plugin", version.ref = "kmp-compose" }
[plugins]kotlin-multiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }plugins { alias(libs.plugins.kotlin.multiplatform) kotlin("native.cocoapods")}
kotlin { androidTarget { compilations.all { kotlinOptions.jvmTarget = "17" } } iosX64() iosArm64() iosSimulatorArm64()
cocoapods { summary = "Shared business logic" homepage = "https://github.com/myorg/myapp" version = "1.0" ios.deploymentTarget = "15.0" framework { baseName = "Shared" } }
sourceSets { commonMain.dependencies { implementation(libs.ktor.client.core) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.json) implementation(libs.sqldelight.runtime) } androidMain.dependencies { implementation(libs.ktor.client.okhttp) implementation(libs.sqldelight.android.driver) } iosMain.dependencies { implementation(libs.ktor.client.darwin) implementation(libs.sqldelight.native.driver) } }}A Flutter adoption at scale is a different conversation — it’s a new app. That’s not always wrong, but it’s never incremental.
When Flutter Is the Right Call
Flutter wins when:
- You have no existing mobile codebase — greenfield, small team, fast MVP, UI consistency across platforms is a feature not a bug
- Your app is a custom UI showcase — games, branded experiences, animation-heavy apps where platform idioms are intentionally bypassed
- Team is already Dart-fluent — the language cost is already paid
- You genuinely need one shared UI — internal tooling, dashboards, admin panels where iOS/Android parity matters more than native feel
Be honest about which of these actually apply. Most B2C product teams are not building custom UI showcases.
The Long-Term Bet
JetBrains and Google are co-investing in KMP. Kotlin is the official Android language. Swift interop has improved dramatically with each release cycle. Compose Multiplatform extends shared UI to desktop and web when needed. The trajectory is clear.
Flutter is a strong framework, well-maintained, with a large community. But it’s a single-vendor bet on a language (Dart) with no footprint outside Flutter, a rendering model that will always lag native platform updates, and an ecosystem that can’t match the combined depth of the JVM and Apple ecosystems.
For teams building production consumer apps where quality, platform fidelity, and long-term maintainability matter — KMP is the compounding bet.