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
Optimizing Flutter UI Performance: Best Practices for Date Formatting and Expensive Operations
Developers often face performance bottlenecks when performing expensive operations like date formatting directly within Flutter's `build` method, especially in fast-scrolling lists. This post will delve into common pitfalls, explain why these operations are costly, and provide practical strategies for optimizing UI performance by caching formatters, using `initState`, and leveraging `compute` for background processing without blocking the UI.
Optimizing Your Flutter Dev Setup: IDEs, Simulators, and AI Tools for Peak Productivity
Flutter developers frequently seek to refine their development environments. This post will dive into popular IDE choices like VS Code and Android Studio, discuss best practices for managing iOS and Android simulators (including in-IDE options), and explore the practical integration of AI tools for code generation and problem-solving to boost overall efficiency.
Demystifying Flutter Performance: Practical Strategies for Large-Scale Apps
Flutter's performance is often blamed for issues in complex applications, but the real culprits are usually architectural decisions, inefficient widget rebuilds, and unoptimized resource handling. This post will dive into common performance bottlenecks in large Flutter apps, providing actionable strategies for profiling, optimizing state management, handling images and network requests efficiently, and leveraging CI/CD for continuous performance monitoring.