Flutter vs. KMP: Choosing the Right Cross-Platform Framework for Your Existing Native App
The Flutter news you actually need
No spam, ever. Unsubscribe in one click.
You have a native app. It works. Your team knows the platform-specific intricacies. Now, the business wants to expand reach or improve development efficiency by going cross-platform. The two leading contenders for a team in your position are Flutter and Kotlin Multiplatform (KMP). This isn’t a greenfield project, so the choice has profound implications for your existing code, team skills, and long-term maintenance. Let’s break it down.
The Core Philosophy: Rewrite vs. Reuse
The fundamental difference lies in the approach to your existing investment.
- Flutter is a complete UI toolkit. You build the entire interface (and most logic) in Dart, rendering to a canvas. It’s a strategic rewrite of the presentation and business layers for a consistent, high-performance experience across platforms.
- Kotlin Multiplatform is a business logic sharing technology. You write your core logic (networking, models, business rules) in Kotlin once and share it across iOS, Android, and others. The UI remains native (Swift/UIKit for iOS, Kotlin/Jetpack Compose for Android). It’s a tactical reuse of your core app.
Evaluating Your Team and Codebase
Your team has an iOS dev (Obj-C) and an Android dev (XML-based UI). This setup is crucial.
For Flutter:
- Pros: You both learn a new, singular technology (Dart/Flutter) from a similar starting point. It creates a unified team. The developer experience with hot reload, a single codebase, and a rich widget library is excellent. UI inconsistencies vanish.
- Cons: It’s a full rewrite. Your 7-year iOS veteran’s deep platform knowledge becomes less critical in the short term. The Android dev’s XML/View expertise doesn’t directly translate. You must also ensure Flutter’s plugins support your Bluetooth/WiFi device connectivity needs.
For KMP:
- Pros: Your Android dev can leverage Kotlin skills immediately. You incrementally extract and share business logic, preserving your native UI investments. It’s less disruptive initially.
- Cons: Your iOS dev, working in Objective-C, faces a steeper integration curve with Kotlin-generated frameworks. You now maintain three codebases: shared Kotlin, native Android UI, and native iOS UI. The “write once, run anywhere” promise is for logic, not UI.
The Bluetooth/WiFi Connectivity Test
This is a key practical hurdle. Let’s see how each framework handles a common Bluetooth operation.
In Flutter, you rely on community or official plugins. You write platform-agnostic Dart code.
import 'package:flutter_blue_plus/flutter_blue_plus.dart';
class DeviceScanner {
final FlutterBluePlus _flutterBlue = FlutterBluePlus.instance;
Future<List<BluetoothDevice>> scanForDevices() async {
// Start scanning
_flutterBlue.startScan(timeout: Duration(seconds: 10));
// Listen to results
return await _flutterBlue.scanResults
.firstWhere((results) => results.isNotEmpty)
.then((results) => results.map((r) => r.device).toList());
}
Future<void> connectToDevice(BluetoothDevice device) async {
await device.connect();
print('Connected to ${device.name}');
}
}
You must vet the plugin (flutter_blue_plus in this case) for stability, API coverage, and maintenance.
In KMP, you write the core scanning/connection logic in Kotlin, but the platform-specific implementation (using CoreBluetooth on iOS, Android Bluetooth APIs) lives in expect/actual declarations.
// In shared KMP module (CommonMain)
expect class PlatformBluetoothScanner {
fun startScan(deviceDiscovered: (DiscoveredDevice) -> Unit)
fun stopScan()
}
data class DiscoveredDevice(val name: String, val identifier: String)
// Android-specific implementation (AndroidMain)
actual class PlatformBluetoothScanner actual constructor() {
private val bluetoothAdapter: BluetoothAdapter? = ...
actual fun startScan(deviceDiscovered: (DiscoveredDevice) -> Unit) {
// Implement using Android Bluetooth APIs
}
actual fun stopScan() { /* Android impl */ }
}
// iOS-specific implementation (iOSMain)
actual class PlatformBluetoothScanner actual constructor() {
private val centralManager: CBCentralManager = ...
actual fun startScan(deviceDiscovered: (DiscoveredDevice) -> Unit) {
// Implement using CoreBluetooth
}
actual fun stopScan() { /* iOS impl */ }
}
KMP gives you fine-grained control but requires you to write and maintain the platform-specific bridges yourself.
Common Pitfalls to Avoid
- Underestimating Platform Channels (Flutter): Assuming Flutter plugins exist for every obscure native SDK. For complex native integrations, you may need to write custom platform channels, which requires native knowledge.
- Over-Promising Shared Code (KMP): Expecting 80%+ code sharing from day one. Start small (models, repositories). Sharing UI state logic is possible with libraries like
compose-multiplatform, but that’s a larger shift. - Ignoring Team Morale: Forcing a senior iOS dev into a pure Kotlin logic role or an Android dev into a full Dart rewrite without buy-in leads to friction. Let each team member prototype their preferred option.
A Practical Decision Framework
Ask your team these questions:
- What’s the primary goal? Uniform UI/UX and fastest feature parity → lean Flutter. Code reuse and preserving native feel → lean KMP.
- Can we afford a rewrite? If the app is large and complex, a full Flutter rewrite is a major project. KMP allows a gradual migration.
- Where is our complexity? If it’s mostly in complex, custom UI, Flutter’s widget system is a powerhouse. If it’s in intricate business logic and device communication, KMP’s logic sharing shines.
- What does our team want to learn? A unified Dart future, or a split path of Kotlin logic + native UI?
The Verdict
There is no universally “right” answer. For your specific scenario—with an Objective-C codebase and a need for deep Bluetooth/WiFi integration—the path of least initial resistance might be Kotlin Multiplatform. It allows your Android dev to drive the shared module while your iOS dev modernizes the UI layer to Swift/SwiftUI, integrating the shared Kotlin code.
However, if your long-term vision is a unified team, a single codebase, and freedom from native platform UI cycles, Flutter is the more transformative and ultimately simpler bet. The investment in learning Dart and rewriting the UI pays dividends in velocity and consistency.
Actionable Next Step: Don’t debate in the abstract. Spend one week on a spike. Have your Android dev build a simple screen (with a mock Bluetooth scan) in Flutter. Have your iOS dev integrate a small, shared KMP module (like a date formatter) into the existing app. The hands-on experience will reveal more than any blog post. The right framework is the one your team can successfully execute and maintain for the next five years.
This blog is produced with the assistance of AI by a human editor. Learn more
Related Posts
Localizing Dynamic Content in Flutter: A Guide to Backend-Driven Translations
Many Flutter apps need to display content that changes based on user locale, but also comes from a backend (like Firebase). This post will explore best practices for fetching and integrating dynamic, localized content from a backend, ensuring a seamless user experience across different languages and regions without hardcoding translations.
Unraveling Type Mismatch Errors in Flutter: A Guide to 'X can't be assigned to Y' and '_InternalLinkedHashMap' Issues
Developers frequently encounter cryptic type mismatch errors like 'The argument type X can't be assigned to the parameter type Y' or '_InternalLinkedHashMap has no instance method 'cast''. This post will demystify these common Flutter/Dart type errors, explain their root causes (e.g., conflicting imports, dynamic typing pitfalls, JSON deserialization issues), and provide practical solutions to diagnose and fix them, improving code robustness and reducing debugging time.
Solving Flutter Web Memory Leaks: A Practical Guide to Identifying and Fixing Performance Issues
Flutter Web applications can suffer from increasing memory usage over time, leading to performance degradation. This post will delve into common causes of memory leaks in Flutter Web, provide practical debugging techniques using browser developer tools and Dart DevTools, and offer actionable strategies to identify and fix these issues for a smoother user experience.