My year of development in 2025

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

aicareers.greenrobot.com

launchday.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.

Kotlin Multiplatform vs Flutter: A Deep Dive into Async Programming

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:

  1. No Shared Memory: Isolates can’t share objects – everything must be serialized
  2. Message Passing Overhead: All communication requires serialization/deserialization
  3. Platform API Restrictions: Isolates can’t directly access many platform APIs
  4. 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:

  1. Shared Memory: Coroutines share the same memory space
  2. Zero Serialization Overhead: Direct object access
  3. Full Platform API Access: Can call any platform API directly
  4. 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.Main gives 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:

  1. Entitlement Inheritance: Isolates may not properly inherit app entitlements
  2. File System Access: Sandbox file access can be restricted in isolates
  3. Network Permissions: Some network operations require main isolate
  4. 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:

  1. Engine Initialization: Each window requires full Flutter engine startup (1-2 seconds)
  2. Black Screen: Visible delay before content appears
  3. Memory Overhead: Each engine instance consumes significant memory
  4. State Isolation: Windows can’t easily share state
  5. 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:

  1. Instant Creation: Windows appear immediately (milliseconds)
  2. No Black Screen: Content is ready before window appears
  3. Shared Framework: KMP framework is loaded once, shared across windows
  4. Native Performance: Uses platform-native windowing APIs
  5. 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:

  1. Isolate Restrictions: Can’t use platform APIs in isolates
  2. Serialization Overhead: Passing data between isolates requires serialization
  3. Complex Error Handling: Errors must be serialized across isolates
  4. 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:

  1. Full Platform Access: Can use any platform API from coroutines
  2. Zero Serialization: Direct object access, no overhead
  3. Structured Concurrency: Automatic cancellation and resource management
  4. 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:

  1. Secure Storage: Isolates can’t access Keychain – had to use workarounds like SSH localhost for macOS sandbox builds (works fine on main thread)
  2. New Windows: 1-2 second delay, black screen, poor UX
  3. State Management: Complex synchronization between windows
  4. Platform Integration: Limited by isolate restrictions
  5. Performance: Higher memory usage, slower window creation
  6. 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:

  1. Secure Storage: Direct Keychain access appears straightforward, no workarounds needed
  2. New Windows: Instant creation in initial testing, no delay, native feel
  3. State Management: Shared ViewModels look simple and efficient
  4. Platform Integration: Full access to all macOS APIs without restrictions
  5. 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

  1. Coroutines vs Isolates: Coroutines provide better performance, lower overhead, and full platform access
  2. Memory Model: Shared memory eliminates serialization overhead
  3. Platform Integration: Direct access to all platform APIs without restrictions
  4. Structured Concurrency: Automatic resource management and cancellation

Developer Experience

  1. Simplicity: Easier to reason about and debug
  2. Performance: Faster operations, lower memory usage
  3. Native Feel: Windows and UI feel truly native
  4. Less Code: Simpler implementations for complex features

User Experience

  1. Performance: Instant window creation, no delays
  2. Responsiveness: Smooth async operations
  3. Native Feel: Feels like a true macOS app
  4. 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.

Kotlin Asynchronous Programming in Kotlin Multiplatform: A Comprehensive Guide

Introduction

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.

Table of Contents

  1. Understanding Kotlin Coroutines
  2. Suspend Functions and async/await
  3. Kotlin Flow: Reactive Streams
  4. Bridging to Swift
  5. Practical Example: Slideshow Generation
  6. Flow vs RxJava vs Swift AsyncSequence
  7. Best Practices

Understanding Kotlin Coroutines

What are Coroutines?

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:

  • Suspendable: Coroutines can pause execution without blocking a thread
  • Structured Concurrency: Parent coroutines manage child coroutines
  • Lightweight: Thousands of coroutines can run on a single thread

Basic Coroutine Example

import kotlinx.coroutines.*

// Launch a coroutine
fun main() = runBlocking {
    launch {
        delay(1000L) // Suspend for 1 second
        println("World!")
    }
    println("Hello,")
}
// Output: Hello, World!

Coroutine Builders

  1. launch: Fire-and-forget, returns a Job
  2. async: Returns a Deferred (like a Future)
  3. runBlocking: Blocks the current thread (use sparingly)
  4. coroutineScope: Creates a scope for structured concurrency
suspend 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)
}

Suspend Functions and async/await

Suspend Functions

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")
}

How Suspend Functions Work

  1. Compilation: The compiler transforms suspend functions into state machines
  2. Continuation: Each suspend point stores its continuation (where to resume)
  3. Non-blocking: The thread is freed when suspended, allowing other work

Bridging to Swift

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!


Kotlin Flow: Reactive Streams

What is Flow?

Flow is Kotlin’s answer to reactive streams (like RxJava’s Observable). It’s a cold stream that emits values over time.

Key Characteristics:

  • Cold: Doesn’t start until collected (lazy)
  • Coroutine-based: Built for Kotlin coroutines
  • Backpressure: Handled automatically
  • Cancellable: Can be cancelled via coroutine cancellation

Flow vs Other Approaches

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

Creating a Flow

import kotlinx.coroutines.flow.*

fun numbersFlow(): Flow<Int> = flow {
    for (i in 1..5) {
        delay(100)
        emit(i) // Emit a value
    }
}

Collecting from Flow

suspend fun collectNumbers() {
    numbersFlow().collect { number ->
        println(number) // Prints 1, 2, 3, 4, 5
    }
}

Flow Operators

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

StateFlow and SharedFlow

  • StateFlow: Holds a single value, emits updates (like LiveData)
  • SharedFlow: Hot flow that can have multiple collectors
val stateFlow = MutableStateFlow(0)

// Collect updates
stateFlow.collect { value ->
    println("Current value: $value")
}

// Update value
stateFlow.value = 1 // Triggers collection

Bridging to Swift

Suspend Functions → Swift async/await

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 → Swift AsyncSequence

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)
}

Practical Example: Slideshow Generation

Let’s see how we use these concepts in our slideshow app:

1. Suspend Functions for Single Operations

// 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
        )
    }
}

2. Concurrent Execution with async/await

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
    )
}

3. Flow for Progress Updates

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))
}

4. Swift Integration

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
                }
            }
        }
    }
}

5. UI Updates

struct ProgressView: View {
    @ObservedObject var viewModel: SlideshowViewModel
    
    var body: some View {
        if let progress = viewModel.generationProgress {
            ProgressView(value: progress.progress) {
                Text(progress.message)
            }
        }
    }
}

Flow vs RxJava vs Swift AsyncSequence

When to Use Each

Use Kotlin Flow when:

  • You need reactive streams in Kotlin
  • You want coroutine-based concurrency
  • You need automatic backpressure handling
  • You’re building a KMP app

Use RxJava when:

  • You’re in a pure JVM/Android environment
  • You need hot observables
  • You have existing RxJava code
  • You need complex operators not in Flow

Use Swift AsyncSequence when:

  • You’re in pure Swift code
  • You need Swift-native async iteration
  • You’re not using KMP

Comparison Table

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)

Code Comparison

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)
}

Best Practices

1. Use Structured Concurrency

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!
}

2. Handle Errors Properly

fun dataFlow(): Flow<Result<Data>> = flow {
    try {
        emit(Result.success(fetchData()))
    } catch (e: Exception) {
        emit(Result.failure(e))
    }
}

3. Use Flow for Multiple Values

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

4. Bridge Flow to Swift Correctly

// ✅ 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

5. Update UI on Main Thread

for try await progress in progressStream {
    await MainActor.run {
        // Update UI properties here
        self.progress = progress
    }
}

6. Cancel Operations When Needed

// In a ViewModel or similar
private var generationJob: Job? = null

fun startGeneration() {
    generationJob = viewModelScope.launch {
        flow.collect { /* ... */ }
    }
}

fun cancelGeneration() {
    generationJob?.cancel()
}

Key Takeaways

  1. Coroutines are Kotlin’s lightweight concurrency mechanism
  2. Suspend functions bridge seamlessly to Swift async/await
  3. Flow is Kotlin’s reactive stream solution, perfect for progress updates
  4. Flow.collect bridges to Swift via AsyncThrowingStream
  5. Structured concurrency ensures proper resource management
  6. Use Flow for multiple values, suspend functions for single values

Real-World Example Summary

In our slideshow app:

  • Suspend functions handle single operations (search images, generate content)
  • async/await with coroutineScope runs operations concurrently
  • Flow emits progress updates as slides are generated
  • Swift AsyncThrowingStream bridges Flow to Swift
  • @Published properties in SwiftUI react to progress updates

This creates a reactive, non-blocking user experience where the UI updates in real-time as work progresses.


Further Reading


This article was written based on implementation in a real Kotlin Multiplatform slideshow generation application.