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
- Understanding Kotlin Coroutines
- Suspend Functions and async/await
- Kotlin Flow: Reactive Streams
- Bridging to Swift
- Practical Example: Slideshow Generation
- Flow vs RxJava vs Swift AsyncSequence
- 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
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 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
- Compilation: The compiler transforms suspend functions into state machines
- Continuation: Each suspend point stores its continuation (where to resume)
- 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
- Coroutines are Kotlin’s lightweight concurrency mechanism
- Suspend functions bridge seamlessly to Swift async/await
- Flow is Kotlin’s reactive stream solution, perfect for progress updates
- Flow.collect bridges to Swift via AsyncThrowingStream
- Structured concurrency ensures proper resource management
- 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
- Kotlin Coroutines Guide
- Kotlin Flow Documentation
- Kotlin Multiplatform Documentation
- Swift Concurrency
This article was written based on implementation in a real Kotlin Multiplatform slideshow generation application.