KMP vs Flutter — Strategic Comparison for Senior Engineers
Choose the Right Cross-Platform Strategy for Your Team
Open interactive version (quiz + challenge)Real-world analogy
What is it?
KMP (Kotlin Multiplatform) and Flutter represent two fundamentally different approaches to cross-platform development. Flutter shares everything (UI + logic) using its own rendering engine and Dart language. KMP shares business logic in Kotlin while keeping native UI (Jetpack Compose + SwiftUI). This lesson provides a senior engineer's framework for choosing between them based on team composition, product requirements, existing codebase, and long-term strategy.
Real-world relevance
This comparison matters in real engineering decisions: startups choosing their initial tech stack, enterprises migrating legacy native apps, CTOs planning team structure, and senior engineers advising on architecture. Companies like Google actually use both — KMP for some Android shared logic and Flutter for consumer apps like Google Pay. The answer is never 'X is always better' — it's 'X is better for YOUR situation.'
Key points
- Kotlin Multiplatform Architecture — expect/actual — KMP uses a shared Kotlin module for business logic (networking, data, domain) that compiles to JVM bytecode (Android), native binary (iOS via Kotlin/Native), and JavaScript (web). Platform-specific code uses expect/actual declarations: you declare an expected interface in shared code and provide actual implementations per platform. This is fundamentally different from Flutter — KMP shares logic, not UI. The native UI layers (Jetpack Compose on Android, SwiftUI on iOS) remain fully platform-native.
- Shared Business Logic vs Shared UI — KMP's core philosophy: share what's invisible to users (networking, caching, business rules, data models) but keep UI native. Flutter's philosophy: share everything including UI. KMP gives 60-70% code sharing, Flutter gives 90-95%. The tradeoff is clear: KMP apps feel perfectly native because they ARE native UI, but you write UI twice. Flutter apps have one UI codebase but may not match platform conventions exactly. In interviews, present both sides without bias.
- Compose Multiplatform — Status & Limitations — Compose Multiplatform (by JetBrains) brings shared UI to KMP using Jetpack Compose syntax. It's stable for Android and Desktop, beta for iOS, and alpha for web. On iOS, it renders via Skia (like Flutter) — NOT native UIKit. Current limitations: no native iOS feel (no UINavigationController transitions, no native text selection), limited iOS accessibility, and smaller ecosystem than Flutter. It's converging toward Flutter's model but is years behind in maturity.
- When to Choose KMP Over Flutter — Choose KMP when: 1) You have existing native Android/iOS apps and want to share business logic without rewriting UI, 2) Platform-native look and feel is non-negotiable (banking, health apps), 3) Your team has strong Kotlin/Swift skills, 4) You need deep platform integration (HealthKit, ARKit, Android Automotive), 5) You're an enterprise with separate iOS and Android teams willing to share a common module. KMP is evolutionary, Flutter is revolutionary.
- When to Choose Flutter Over KMP — Choose Flutter when: 1) Starting a new app from scratch with a small team, 2) Pixel-perfect custom UI that's identical across platforms, 3) Rapid prototyping and MVP speed is critical, 4) Your team knows Dart or is willing to learn (simpler than Kotlin + Swift), 5) You need web + desktop + mobile from one codebase, 6) The app is content/utility focused rather than platform-convention heavy. Flutter's single codebase = lower maintenance cost long-term.
- Migration Strategies Between Platforms — Flutter to native: Extract business logic into a Dart package, gradually replace UI screens with native views using platform channels. KMP can consume Flutter modules via method channels during transition. Native to Flutter: Use add-to-app to embed Flutter screens in existing native apps — migrate screen by screen. KMP to Flutter: Rewrite UI in Flutter, port shared Kotlin logic to Dart (they're syntactically similar). Any migration should be incremental — big-bang rewrites fail.
- Team Composition & Hiring Considerations — Flutter: Need Dart developers (smaller pool but growing fast). One team builds everything. Faster feature velocity with smaller teams. KMP: Need Kotlin developers (large pool) + iOS developers for SwiftUI (expensive, scarce). Two UI teams + shared module team. Higher headcount but deeper platform expertise. Senior interview insight: Flutter reduces bus factor and communication overhead. KMP requires more coordination but produces more platform-polished results.
- Performance & Native Feel Comparison — Raw rendering: Flutter and Compose Multiplatform both use Skia/Impeller — similar performance. KMP with native UI has zero rendering overhead — it IS native. Startup time: Native/KMP wins (no engine initialization). Memory: Flutter's Dart VM adds ~20-30MB baseline. Animations: Flutter's consistent 60fps is excellent, but native platforms have system animations that Flutter must manually replicate. For 95% of apps, performance difference is imperceptible.
- Ecosystem & Package Availability — Flutter: 40,000+ packages on pub.dev, strong community, Google backing. Most common integrations exist. KMP: Smaller shared-module ecosystem, but has access to the ENTIRE native ecosystem (CocoaPods, Maven). If a native SDK exists, KMP can use it directly. Flutter must wrap native SDKs in platform channels. For rare/specialized SDKs (medical devices, hardware), KMP has an inherent advantage.
- Long-term Viability & Industry Trends — Both are backed by major companies: Flutter by Google, KMP by JetBrains (with Google endorsing it for Android). Google officially recommends KMP for shared business logic in Android apps. Flutter is expanding to embedded/automotive. The trend is convergence: KMP is adding shared UI (Compose Multiplatform), and Flutter is improving platform integration. In 5 years, the line between them may blur. Senior advice: bet on the approach that matches your current team and product needs.
- Real-World Adoption Comparison — Flutter: Google Pay, BMW, Toyota, eBay, Alibaba, Nubank (largest digital bank outside Asia). KMP: Netflix (shared logic), Cash App (Square), Philips, VMware, Autodesk. Pattern: consumer apps with custom UI lean Flutter, enterprise/fintech with strict platform requirements lean KMP. Many companies use BOTH: KMP for business logic modules and Flutter for specific features.
- Decision Framework for Technical Leaders — Use this matrix: Team size <5 = Flutter. Existing native apps = KMP. New greenfield app = Flutter. Platform conventions critical = KMP. Web + mobile + desktop = Flutter. Deep hardware integration = KMP. Time to market priority = Flutter. Separate iOS/Android teams already exist = KMP. The worst decision is no decision — analysis paralysis costs more than either approach's tradeoffs.
Code example
// ============================================
// Side-by-side: Same feature in KMP vs Flutter
// ============================================
// --- KMP Shared Module (Kotlin) ---
// commonMain/src/UserRepository.kt
//
// class UserRepository(
// private val api: UserApi,
// private val cache: UserCache,
// ) {
// suspend fun getUser(id: String): Result<User> {
// return cache.get(id) ?: api.fetchUser(id).also {
// cache.put(id, it)
// }
// }
// }
//
// // expect declaration — platform provides actual
// expect class UserCache() {
// fun get(id: String): User?
// fun put(id: String, user: User)
// }
// --- KMP iOS UI (Swift) ---
// struct UserProfileView: View {
// @StateObject var viewModel = UserViewModel()
// var body: some View {
// NavigationView {
// // Fully native SwiftUI — uses iOS conventions
// }
// }
// }
// --- KMP Android UI (Kotlin) ---
// @Composable
// fun UserProfileScreen(viewModel: UserViewModel) {
// // Fully native Jetpack Compose — Material 3
// }
// ============================================
// --- Flutter Equivalent (Dart) ---
// ============================================
// Shared repository (same logic, one language)
class UserRepository {
final UserApi _api;
final UserCache _cache;
UserRepository({required UserApi api, required UserCache cache})
: _api = api,
_cache = cache;
Future<User> getUser(String id) async {
final cached = _cache.get(id);
if (cached != null) return cached;
final user = await _api.fetchUser(id);
_cache.put(id, user);
return user;
}
}
// Shared UI (one codebase, all platforms)
class UserProfileScreen extends StatelessWidget {
final String userId;
const UserProfileScreen({super.key, required this.userId});
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Profile')),
body: FutureBuilder<User>(
future: context.read<UserRepository>().getUser(userId),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
return Center(
child: Text('Error: ${snapshot.error}'),
);
}
final user = snapshot.data!;
return _buildProfile(user);
},
),
);
}
Widget _buildProfile(User user) {
return ListView(
padding: const EdgeInsets.all(16),
children: [
CircleAvatar(radius: 50, backgroundImage: NetworkImage(user.avatarUrl)),
const SizedBox(height: 16),
Text(user.name, style: const TextStyle(fontSize: 24)),
Text(user.email, style: const TextStyle(color: Colors.grey)),
const SizedBox(height: 24),
// Platform-adaptive: Material on Android, Cupertino on iOS
if (Platform.isIOS)
CupertinoButton(
child: const Text('Edit Profile'),
onPressed: () => _editProfile(user),
)
else
ElevatedButton(
onPressed: () => _editProfile(user),
child: const Text('Edit Profile'),
),
],
);
}
void _editProfile(User user) {
// Navigate to edit screen
}
}
// ============================================
// Decision Helper — Use in architecture docs
// ============================================
enum ProjectCharacteristic {
newGreenfield,
existingNativeApps,
smallTeam,
separatePlatformTeams,
customBrandedUI,
platformNativeUICritical,
needsWebDesktop,
deepHardwareIntegration,
rapidPrototyping,
enterpriseCompliance,
}
String recommendApproach(Set<ProjectCharacteristic> traits) {
int flutterScore = 0;
int kmpScore = 0;
final scoring = {
ProjectCharacteristic.newGreenfield: (2, 0),
ProjectCharacteristic.existingNativeApps: (0, 3),
ProjectCharacteristic.smallTeam: (3, 0),
ProjectCharacteristic.separatePlatformTeams: (0, 3),
ProjectCharacteristic.customBrandedUI: (3, 1),
ProjectCharacteristic.platformNativeUICritical: (0, 3),
ProjectCharacteristic.needsWebDesktop: (3, 1),
ProjectCharacteristic.deepHardwareIntegration: (0, 2),
ProjectCharacteristic.rapidPrototyping: (3, 0),
ProjectCharacteristic.enterpriseCompliance: (1, 2),
};
for (final trait in traits) {
final (f, k) = scoring[trait]!;
flutterScore += f;
kmpScore += k;
}
if (flutterScore > kmpScore + 3) return 'Strong Flutter recommendation';
if (kmpScore > flutterScore + 3) return 'Strong KMP recommendation';
if (flutterScore > kmpScore) return 'Slight Flutter lean — evaluate team skills';
if (kmpScore > flutterScore) return 'Slight KMP lean — evaluate team skills';
return 'Either works — decide based on team expertise';
}Line-by-line walkthrough
- 1. The KMP shared module shows UserRepository in Kotlin — this exact code compiles for both Android and iOS. The business logic (fetch, cache) is written once.
- 2. The expect class UserCache declares what's needed without implementation — each platform provides its actual version (NSUserDefaults on iOS, SharedPreferences on Android).
- 3. KMP's iOS UI is pure SwiftUI — it calls the shared Kotlin module but renders with fully native iOS components and navigation patterns.
- 4. KMP's Android UI is pure Jetpack Compose — same shared logic, native Material 3 rendering. Two UI codebases, one logic layer.
- 5. Flutter's UserRepository is functionally identical to KMP's — similar syntax (Dart and Kotlin are both modern languages), but it's one codebase for everything.
- 6. Flutter's UserProfileScreen is shared across ALL platforms — one Widget tree renders on iOS, Android, web, and desktop.
- 7. The Platform.isIOS check shows Flutter's approach to platform adaptation — you CAN make things look native, but it requires explicit conditional code.
- 8. The decision helper enum codifies project characteristics that influence the Flutter vs KMP choice — this is useful for real architecture discussions.
- 9. The scoring system weights each characteristic — smallTeam and rapidPrototyping strongly favor Flutter, while existingNativeApps and separatePlatformTeams favor KMP.
- 10. The recommendation function returns nuanced advice — it acknowledges that close scores mean team expertise should be the tiebreaker, not framework features.
Spot the bug
// Common mistakes in cross-platform architecture decisions
// Bug 1: Assuming Flutter can't look native
class ProfileScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
// Always using Material, even on iOS
return Scaffold(
appBar: AppBar(title: Text('Profile')),
body: Column(
children: [
ElevatedButton(
onPressed: () {},
child: Text('Settings'),
),
],
),
);
}
}
// Bug 2: Wrong KMP assumption — sharing UI code
// Expecting KMP to share this Composable on iOS
// @Composable
// fun SharedScreen() {
// MaterialTheme { Text("This runs on iOS too!") }
// // ^ This only works with Compose Multiplatform (beta on iOS)
// // Standard KMP does NOT share UI
// }
// Bug 3: Ignoring platform channels exist
// "Flutter can't access native APIs"
// This is wrong — platform channels bridge any native APINeed a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Kotlin Multiplatform Official Documentation (kotlinlang.org)
- Flutter vs KMP — Google's Official Perspective (developer.android.com)
- KMP vs Flutter — Pragmatic Comparison (YouTube)
- Compose Multiplatform iOS Status (JetBrains)