Back to blog

KMP vs Flutter: Why I'd Choose Kotlin Multiplatform for Mobile Development

A pragmatic KMP vs Flutter comparison for production mobile apps — covering code sharing strategy, platform fidelity, ecosystem depth, hiring, and long-term maintainability.

KMP vs Flutter: Why I'd Choose Kotlin Multiplatform for Mobile Development

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

shared/src/commonMain/kotlin/com/myapp/domain/AuthRepository.kt
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.kt
class 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 — Swift
import 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

DimensionKMPFlutter
Android toolingNative — AS, Gradle, ProfilerVia plugin — functional but abstracted
iOS toolingNative — Xcode, Instruments, TestFlightVia flutter build — Xcode mostly hidden
Language on AndroidKotlin (first-class)Dart (separate language to hire for)
Language on iOSSwift (first-class)Dart (Swift interop via method channels)
Existing Android codebaseGradual adoption — no rewriteFull rewrite or parallel app
> Existing iOS codebaseGradual adoption — share logic incrementallyFull rewrite or parallel app
Hiring poolKotlin/Swift engineers (large)Dart engineers (smaller, Flutter-specific)
Library ecosystemEntire JVM + iOS ecosystems availableFlutter pub.dev — growing but narrower
Google support longevityBacked by Google + JetBrains + AOSPBacked 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:

  1. Extract one domain module — say, UserPreferences or AnalyticsTracker
  2. Wire the shared Kotlin artifact into your existing Android and iOS apps
  3. Validate the approach works in your CI/CD, your team’s workflow, your release pipeline
  4. Expand the shared surface area over successive sprints
gradle/libs.versions.toml
[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" }
shared/build.gradle.kts
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.

Previous article Hetzner vs AWS, GCP, and Azure: Why I Run Most Projects on Hetzner Next article Why a Modular Monolith Might Be the Best Architecture for Your Startup

Related articles