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
Flutter App Size Optimization: Strategies to Shrink Your APK/IPA Below 10MB
Flutter apps are often criticized for their larger-than-native binary sizes. This post will dive into practical, actionable strategies to significantly reduce your Flutter app's footprint, including build configurations, asset optimization, and dependency management, demonstrating how to achieve app sizes under 10MB.
Mastering In-App Subscriptions in Flutter: A Guide to Avoiding Common Pitfalls
Implementing in-app subscriptions in Flutter can be surprisingly complex, especially when dealing with cross-platform nuances, receipt validation, and managing different store configurations. This post will delve into the common challenges developers face, compare solutions like RevenueCat with custom implementations, and provide best practices for setting up and maintaining a robust subscription system.
Taming Android Emulator RAM: A Developer's Guide to Reducing Memory Usage for Flutter
Android emulators are notorious for hogging RAM, often pushing developer machines into swap and slowing down Flutter development. This post will explore the reasons behind high emulator memory consumption and provide practical, actionable strategies and tools (including a custom-built solution) to significantly reduce RAM usage, ensuring a smoother development experience without sacrificing functionality.