Today’s work on codefrog in flutter for windows
Sign up for the mailing list to be notified when the Windows version drops.
https://codefrog.app

Today’s work on codefrog in flutter for windows
Sign up for the mailing list to be notified when the Windows version drops.
https://codefrog.app

My year of development in 2025. I had a job up until May. Currently working on codefrog.app for Windows and searching for work and people to develop stuff with.
I created a lot of sites and apps using AI (Cursor, CodeRabbit, AugmentCode, Antigravity, Amp)
march
3dtankbattle.com
3dwebgames.com
may
longevity.greenrobot.com
mentalhealthlawyers.greenrobot.com
remotedevjobs.greenrobot.com
june
robots.greenrobot.com
september
gunstopperdrone.com
game.gunstopperdrone.com
december
codefrog.app
github.com/greenrobotllc/bio-neighbor
Notable accomplishment: Getting CodeFrog.app approved on the App Store and being able to use open-source programs installed on the computer within the app while still sandboxed by utilizing SSH on the localhost. Thank you, Apple, for approving it! I am so grateful!
Unfortunate setback: My loving, kind, good watchdog, Albert, got cancer, and we have an appointment with radiation upcoming in January.
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.
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:
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:
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:
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.
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:
Dispatchers.Main gives access to UI and platform APIsmacOS 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:
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:
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:
Real Experience from codefrog.app:
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:
Real Experience:
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:
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:
Flutter:
KMP:
Flutter:
KMP:
Flutter:
KMP:
Building codefrog.app in Flutter revealed several pain points:
While just getting started with KMP, the initial experience shows promise:
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:
Choose Flutter if:
Choose KMP if:
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.
This article explores how to use Kotlin’s asynchronous programming features in Kotlin Multiplatform (KMP) applications. We’ll cover Coroutines, Flow, async/await, and how they bridge to Swift and other platforms. This guide is based on a real-world implementation in a slideshow generation app.
Coroutines are Kotlin’s way of writing asynchronous, non-blocking code. Think of them as lightweight threads that can be suspended and resumed.
Key Concepts:
import kotlinx.coroutines.*
// Launch a coroutine
fun main() = runBlocking {
launch {
delay(1000L) // Suspend for 1 second
println("World!")
}
println("Hello,")
}
// Output: Hello, World!
launch: Fire-and-forget, returns a Jobasync: Returns a Deferred (like a Future)runBlocking: Blocks the current thread (use sparingly)coroutineScope: Creates a scope for structured concurrencysuspend fun fetchData() = coroutineScope {
val data1 = async { fetchFromSource1() }
val data2 = async { fetchFromSource2() }
// Both run concurrently, await when needed
val result1 = data1.await()
val result2 = data2.await()
combineResults(result1, result2)
}
A suspend function can be paused and resumed. It can only be called from a coroutine or another suspend function.
suspend fun fetchUser(id: String): User {
delay(1000) // Simulate network call
return User(id, "John Doe")
}
Kotlin suspend functions automatically bridge to Swift async functions:
Kotlin:
suspend fun createSlideshow(topic: String): Slideshow {
// ... implementation
}
Swift:
let slideshow = try await kmpBridge.createSlideshow(topic: "Travel Tips")
The Kotlin compiler generates the necessary bridging code automatically!
Flow is Kotlin’s answer to reactive streams (like RxJava’s Observable). It’s a cold stream that emits values over time.
Key Characteristics:
| Feature | Flow | RxJava Observable | Swift AsyncSequence |
|---|---|---|---|
| Hot/Cold | Cold by default | Hot by default | Cold |
| Threading | Coroutine-based | Thread-based | Async/await |
| Backpressure | Automatic | Manual | Automatic |
| Cancellation | Coroutine cancellation | Subscription | Task cancellation |
import kotlinx.coroutines.flow.*
fun numbersFlow(): Flow<Int> = flow {
for (i in 1..5) {
delay(100)
emit(i) // Emit a value
}
}
suspend fun collectNumbers() {
numbersFlow().collect { number ->
println(number) // Prints 1, 2, 3, 4, 5
}
}
Flow provides many operators similar to RxJava:
flowOf(1, 2, 3, 4, 5)
.map { it * 2 } // Transform
.filter { it > 5 } // Filter
.take(2) // Take first 2
.collect { println(it) } // Collect: 6, 8
val stateFlow = MutableStateFlow(0)
// Collect updates
stateFlow.collect { value ->
println("Current value: $value")
}
// Update value
stateFlow.value = 1 // Triggers collection
Kotlin suspend functions bridge seamlessly to Swift:
Kotlin:
suspend fun searchImages(query: String): List<ImageResult>
Swift:
let images = try await kmpBridge.searchImages(query: "nature")
Flow doesn’t bridge directly, but Flow.collect is a suspend function:
Kotlin:
fun generateProgress(): Flow<Progress> = flow {
emit(Progress.Started)
delay(1000)
emit(Progress.Processing(50))
delay(1000)
emit(Progress.Completed)
}
Swift Bridge:
func generateProgress() -> AsyncThrowingStream<ProgressModel, Error> {
return AsyncThrowingStream { continuation in
Task {
do {
let flow = appModule.useCase.generateProgress()
try await flow.collect { progress in
let model = progress.toSwiftModel()
continuation.yield(model)
if progress is Progress.Completed {
continuation.finish()
}
}
} catch {
continuation.finish(throwing: error)
}
}
}
}
Swift Usage:
for try await progress in kmpBridge.generateProgress() {
updateUI(with: progress)
}
Let’s see how we use these concepts in our slideshow app:
// Repository layer
interface ImageRepository {
suspend fun searchImages(query: String): List<ImageResult>
}
// Use case layer
class CreateSlideshowUseCase(
private val imageRepository: ImageRepository
) {
suspend fun createFromTopic(topic: String): Slideshow {
// Sequential execution
val images = imageRepository.searchImages(topic)
val music = musicRepository.suggestMusic(topic)
return Slideshow(
title = topic,
slides = images.map { createSlide(it) },
music = music
)
}
}
suspend fun createFromTopic(topic: String): Slideshow = coroutineScope {
// Launch multiple operations concurrently
val imagesDeferred = async { imageRepository.searchImages(topic) }
val musicDeferred = async { musicRepository.suggestMusic(topic) }
val slideContentDeferred = async { aiRepository.generateSlides(topic) }
// Await all results
val images = imagesDeferred.await()
val music = musicDeferred.await()
val slideContent = slideContentDeferred.await()
// Combine results
Slideshow(
title = topic,
slides = combineSlides(images, slideContent),
music = music
)
}
fun createFromTopicWithProgress(topic: String): Flow<GenerationProgress> = flow {
// Emit progress updates as work progresses
emit(GenerationProgress.GeneratingSlides(0, 5))
val slideContents = aiRepository.generateStructuredSlides(topic, 5)
emit(GenerationProgress.GeneratingSlides(5, 5))
slideContents.forEachIndexed { index, content ->
emit(GenerationProgress.GeneratingImages(index + 1, 5, content.title))
val image = imageRepository.generateImage(content.imageKeywords.first())
// ... create slide
// Emit progress for each slide
}
emit(GenerationProgress.Completed(slideshow))
}
class SlideshowViewModel: ObservableObject {
@Published var generationProgress: GenerationProgressModel?
func createSlideshow(topic: String) async {
let progressStream = kmpBridge.createSlideshowWithProgress(
topic: topic,
numberOfSlides: 5
)
// Iterate over the stream reactively
for try await progress in progressStream {
await MainActor.run {
self.generationProgress = progress
if case .completed(let slideshow) = progress {
self.currentSlideshow = slideshow
}
}
}
}
}
struct ProgressView: View {
@ObservedObject var viewModel: SlideshowViewModel
var body: some View {
if let progress = viewModel.generationProgress {
ProgressView(value: progress.progress) {
Text(progress.message)
}
}
}
}
Use Kotlin Flow when:
Use RxJava when:
Use Swift AsyncSequence when:
| Aspect | Flow | RxJava | AsyncSequence |
|---|---|---|---|
| Platform | Kotlin Multiplatform | JVM/Android | Swift |
| Concurrency Model | Coroutines | Threads | async/await |
| Hot/Cold | Cold | Hot (default) | Cold |
| Backpressure | Automatic | Manual | Automatic |
| Cancellation | Coroutine scope | Subscription | Task |
| Learning Curve | Medium | Steep | Easy (if you know async/await) |
Kotlin Flow:
flowOf(1, 2, 3)
.map { it * 2 }
.collect { println(it) }
RxJava:
Observable.just(1, 2, 3)
.map { it * 2 }
.subscribe { println(it) }
Swift AsyncSequence:
let sequence = AsyncStream { continuation in
for i in 1...3 {
continuation.yield(i * 2)
}
continuation.finish()
}
for await value in sequence {
print(value)
}
✅ Good:
suspend fun processData() = coroutineScope {
val result1 = async { fetchData1() }
val result2 = async { fetchData2() }
combineResults(result1.await(), result2.await())
}
❌ Bad:
suspend fun processData() {
GlobalScope.launch { /* ... */ } // Don't use GlobalScope!
}
fun dataFlow(): Flow<Result<Data>> = flow {
try {
emit(Result.success(fetchData()))
} catch (e: Exception) {
emit(Result.failure(e))
}
}
✅ Use Flow:
fun progressUpdates(): Flow<Progress> = flow {
emit(Progress.Started)
emit(Progress.Processing(50))
emit(Progress.Completed)
}
❌ Don’t use suspend function:
suspend fun getProgress(): Progress // Can only return one value
// ✅ Good: Use AsyncThrowingStream
func getProgress() -> AsyncThrowingStream<ProgressModel, Error> {
return AsyncThrowingStream { continuation in
Task {
try await flow.collect { progress in
continuation.yield(progress.toSwiftModel())
if progress is Completed {
continuation.finish()
}
}
}
}
}
// ❌ Bad: Don't try to use Flow directly in Swift
// Flow doesn't bridge - use collect instead
for try await progress in progressStream {
await MainActor.run {
// Update UI properties here
self.progress = progress
}
}
// In a ViewModel or similar
private var generationJob: Job? = null
fun startGeneration() {
generationJob = viewModelScope.launch {
flow.collect { /* ... */ }
}
}
fun cancelGeneration() {
generationJob?.cancel()
}
In our slideshow app:
This creates a reactive, non-blocking user experience where the UI updates in real-time as work progresses.
This article was written based on implementation in a real Kotlin Multiplatform slideshow generation application.