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.

Leave a Comment