Introduction
As a developer who has built a released application in Flutter, I’ve recently started exploring Kotlin Multiplatform (KMP) for a new project. After building codefrog.app – a macOS developer tool in Flutter – I’ve been evaluating KMP for similar functionality. Even in the early stages, the differences in how these frameworks handle asynchronous programming are immediately apparent. This article explores why KMP’s async programming model appears superior, especially for native platform integration.
The Core Difference: Coroutines vs Isolates
Flutter’s Isolate Model
Flutter uses Isolates for concurrent execution. Isolates are separate memory spaces that communicate via message passing – essentially separate processes that can’t share memory.
// Flutter isolate example
import 'dart:isolate';
void isolateFunction(SendPort sendPort) {
// Heavy computation
sendPort.send(result);
}
void main() async {
ReceivePort receivePort = ReceivePort();
await Isolate.spawn(isolateFunction, receivePort.sendPort);
var result = await receivePort.first;
}
Key Limitations:
- No Shared Memory: Isolates can’t share objects – everything must be serialized
- Message Passing Overhead: All communication requires serialization/deserialization
- Platform API Restrictions: Isolates can’t directly access many platform APIs
- Complex State Management: Sharing state between isolates requires explicit message passing
KMP’s Coroutine Model
KMP uses Kotlin Coroutines – lightweight threads that can suspend and resume without blocking.
// KMP coroutine example
suspend fun fetchData(): Data {
return withContext(Dispatchers.IO) {
// Network call - suspends, doesn't block
api.getData()
}
}
// Usage
viewModelScope.launch {
val data = fetchData() // Seamless async/await
updateUI(data)
}
Key Advantages:
- Shared Memory: Coroutines share the same memory space
- Zero Serialization Overhead: Direct object access
- Full Platform API Access: Can call any platform API directly
- Structured Concurrency: Automatic cancellation and resource management
The Secure Storage Problem
Flutter’s Isolate Limitation
While building codefrog.app, I discovered that isolates cannot access secure storage APIs on macOS. This became a significant issue when porting to Windows, where the app would hang – a problem that doesn’t occur on macOS. I’m still working through the multithreading issues on Windows, which has proven to be very time-consuming.
// This DOESN'T work in an isolate
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
void isolateFunction() {
final storage = FlutterSecureStorage();
// ❌ CRASHES: Secure storage requires main isolate
await storage.write(key: 'token', value: 'secret');
}
Why This Happens:
- macOS Keychain access requires the main thread/isolate
- Isolates run in separate processes without proper entitlements
- Secure storage plugins are designed for the main isolate only
- Workarounds require complex message passing back to main isolate
I discocered the macOS app had no problems doing everything on the main thread not using isoaltes, but when trying to port to Windows it hung, meaning I would have to use isolates on Windows in order to release a Windows Version using Flutter.
KMP’s Seamless Access
In KMP, coroutines can access secure storage directly because they share the same memory space and thread context.
// This WORKS perfectly in a coroutine
suspend fun saveCredentials(token: String) {
withContext(Dispatchers.Main) {
// Direct Keychain access - no restrictions
keychain.save("token", token)
}
}
// Can be called from any coroutine
viewModelScope.launch {
saveCredentials("secret") // Works everywhere!
}
Why This Works:
- Coroutines can switch dispatchers (threads) seamlessly
Dispatchers.Maingives access to UI and platform APIs- No serialization needed – direct object access
- Full platform entitlements and permissions
macOS Sandbox Restrictions
Flutter’s Sandbox Challenges
macOS sandboxed apps have strict security requirements. Flutter isolates face additional challenges:
// Sandboxed app trying to access file system from isolate
void isolateFunction() async {
// ❌ May fail: Isolates don't inherit sandbox entitlements properly
final file = File('/path/to/file');
await file.writeAsString('data');
}
Issues:
- Entitlement Inheritance: Isolates may not properly inherit app entitlements
- File System Access: Sandbox file access can be restricted in isolates
- Network Permissions: Some network operations require main isolate
- User Permissions: System dialogs (file picker, etc.) must be on main isolate
KMP’s Native Integration
KMP coroutines work seamlessly with macOS sandboxing:
// Direct platform API access with proper sandbox support
suspend fun saveFile(data: String) {
withContext(Dispatchers.Main) {
// Full sandbox support - uses app's entitlements
val panel = NSSavePanel()
if (panel.runModal() == .OK) {
data.writeToFile(panel.url.path)
}
}
}
Advantages:
- Coroutines use the app’s main thread context
- All entitlements and permissions are inherited
- Direct access to NSFileManager, Keychain, etc.
- No workarounds needed
Window Management: A Performance Nightmare
Flutter’s Engine-Per-Window Model
One of the most painful experiences building codefrog.app was implementing File → New Window. Flutter’s architecture requires a separate engine instance for each window.
// Flutter: Creating a new window
void createNewWindow() {
// ❌ Must create entire new Flutter engine
final engine = FlutterEngine();
await engine.run(); // Expensive initialization
// ❌ Black screen while engine initializes (1-2 seconds)
final window = await engine.createWindow();
// ❌ Each window has separate memory footprint
// ❌ No shared state between windows
}
Problems:
- Engine Initialization: Each window requires full Flutter engine startup (1-2 seconds)
- Black Screen: Visible delay before content appears
- Memory Overhead: Each engine instance consumes significant memory
- State Isolation: Windows can’t easily share state
- Complex Implementation: Requires platform channel setup for each window
Real Experience from codefrog.app:
- New windows took 1-2 seconds to appear
- Black screen was visible to users (poor UX)
- Memory usage increased linearly with window count
- Had to implement complex state synchronization between windows
- File menu → New Window required significant platform channel work
KMP’s Native Window Management
In KMP with SwiftUI, window creation is trivial and instant:
// KMP/SwiftUI: Creating a new window
func openNewWindow() {
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 800, height: 600),
styleMask: [.titled, .closable, .resizable],
backing: .buffered,
defer: false
)
window.contentView = NSHostingView(rootView: ContentView())
window.makeKeyAndOrderFront(nil)
// ✅ Instant - no engine initialization
// ✅ No black screen
// ✅ Shared framework instance
}
Advantages:
- Instant Creation: Windows appear immediately (milliseconds)
- No Black Screen: Content is ready before window appears
- Shared Framework: KMP framework is loaded once, shared across windows
- Native Performance: Uses platform-native windowing APIs
- Simple Implementation: ~10 lines of code vs. hundreds in Flutter
Real Experience:
- New windows appear instantly
- No visible delay or black screen
- Memory overhead is minimal (shared framework)
- State can be easily shared via shared ViewModels
- File menu → New Window: 5 minutes to implement vs. hours in Flutter
Async Programming Comparison
Flutter: Futures and Streams
Flutter uses Dart’s Future and Stream for async operations:
// Flutter async
Future<List<Data>> fetchData() async {
final response = await http.get(url);
return parseData(response.body);
}
// Stream for reactive updates
Stream<Progress> generateProgress() async* {
yield Progress.started;
await Future.delayed(Duration(seconds: 1));
yield Progress.processing(50);
yield Progress.completed;
}
Issues:
- Isolate Restrictions: Can’t use platform APIs in isolates
- Serialization Overhead: Passing data between isolates requires serialization
- Complex Error Handling: Errors must be serialized across isolates
- No Structured Concurrency: Manual cancellation and cleanup
KMP: Coroutines and Flow
KMP uses Kotlin Coroutines and Flow:
// KMP async
suspend fun fetchData(): List<Data> {
return withContext(Dispatchers.IO) {
val response = client.get(url)
parseData(response.body)
}
}
// Flow for reactive updates
fun generateProgress(): Flow<Progress> = flow {
emit(Progress.Started)
delay(1000)
emit(Progress.Processing(50))
emit(Progress.Completed)
}
Advantages:
- Full Platform Access: Can use any platform API from coroutines
- Zero Serialization: Direct object access, no overhead
- Structured Concurrency: Automatic cancellation and resource management
- Seamless Bridging: Coroutines bridge to Swift async/await automatically
Performance Comparison
Memory Usage
Flutter:
- Each window: ~50-100MB (separate engine)
- Isolates: Additional memory per isolate
- Total for 3 windows: ~200-300MB
KMP:
- Shared framework: ~20-30MB (loaded once)
- Each window: ~5-10MB (just UI)
- Total for 3 windows: ~35-60MB
Window Creation Time
Flutter:
- Engine initialization: 1-2 seconds
- Black screen visible: Yes
- User-perceived delay: High
KMP:
- Window creation: <50ms
- Black screen visible: No
- User-perceived delay: None
Async Operation Overhead
Flutter:
- Isolate communication: Serialization overhead
- Message passing: ~1-5ms per message
- State synchronization: Complex and error-prone
KMP:
- Coroutine context switch: <1ms
- Direct memory access: Zero overhead
- State sharing: Native Swift/Kotlin objects
Real-World Use Case: codefrog.app
The Flutter Experience
Building codefrog.app in Flutter revealed several pain points:
- Secure Storage: Isolates can’t access Keychain – had to use workarounds like SSH localhost for macOS sandbox builds (works fine on main thread)
- New Windows: 1-2 second delay, black screen, poor UX
- State Management: Complex synchronization between windows
- Platform Integration: Limited by isolate restrictions
- Performance: Higher memory usage, slower window creation
- Windows Porting: App hangs on Windows due to multithreading issues – still working on this complex problem
Early KMP Exploration
While just getting started with KMP, the initial experience shows promise:
- Secure Storage: Direct Keychain access appears straightforward, no workarounds needed
- New Windows: Instant creation in initial testing, no delay, native feel
- State Management: Shared ViewModels look simple and efficient
- Platform Integration: Full access to all macOS APIs without restrictions
- Performance: Lower memory footprint and faster operations in early benchmarks
Conclusion: Why KMP Appears Superior for Async Programming
Based on my experience building production apps in Flutter and early exploration of KMP, KMP’s async programming model appears significantly better for native platform integration:
Technical Superiority
- Coroutines vs Isolates: Coroutines provide better performance, lower overhead, and full platform access
- Memory Model: Shared memory eliminates serialization overhead
- Platform Integration: Direct access to all platform APIs without restrictions
- Structured Concurrency: Automatic resource management and cancellation
Developer Experience
- Simplicity: Easier to reason about and debug
- Performance: Faster operations, lower memory usage
- Native Feel: Windows and UI feel truly native
- Less Code: Simpler implementations for complex features
User Experience
- Performance: Instant window creation, no delays
- Responsiveness: Smooth async operations
- Native Feel: Feels like a true macOS app
- Reliability: Fewer edge cases and workarounds
When to Choose Each
Choose Flutter if:
- You need cross-platform mobile (iOS + Android) with web
- You’re building a consumer app with simple async needs
- You don’t need deep platform integration
- Your team is primarily Dart/Flutter developers
Choose KMP if:
- You need native desktop apps (macOS, Windows, Linux)
- You require deep platform integration (Keychain, file system, etc.)
- Performance and memory usage matter
- You want true native feel and behavior
- You’re building developer tools or professional software
Final Thoughts
As someone who has shipped production apps in Flutter and is now exploring KMP, KMP’s async programming model appears to be the clear winner for desktop applications requiring native platform integration. The coroutine model provides better performance, simpler code, and full platform access – things that Flutter’s isolate model simply cannot match.
For codefrog.app and similar developer tools, KMP’s advantages in window management, secure storage access, and overall performance make it an attractive choice. Based on early exploration, it seems the days of waiting 1-2 seconds for new windows and working around isolate limitations could be over.
I’m still in the early stages of my KMP journey, but the initial signs are very promising. The async programming model alone makes it worth serious consideration for any desktop application requiring deep platform integration.
This article is based on real-world experience building codefrog.app in Flutter and early exploration of Kotlin Multiplatform. Performance numbers and observations are from actual Flutter development and initial KMP testing.