What are Flows?
What are flows in general programming and what is special about them in Kotlin Multiplatform (KMP)?
Introduction to Flows in Programming
Core Concepts
At its foundation, a flow in programming represents an asynchronous sequence of values that can be computed and emitted over time. This concept builds upon reactive programming principles, where data is treated as streams that can be observed, transformed, and consumed
- Regular functions give you one answer and stop (like asking "What's 5 + 3?" and getting "8")
- Flows give you many answers over time (like watching a live video stream that keeps sending new frames)
Every flow-based system involves three primary components:
Producer: Generates data and adds it to the stream. Thanks to asynchronous capabilities, producers can generate data without blocking execution threads.
Intermediaries (optional): Transform or modify values emitted into the stream using operators such as map, filter, and transform. These components adjust data to meet downstream requirements.
Consumer: The consumer takes the data from the flow and does something with it.
Key Characteristics
Asynchronous Execution: Flows handle data asynchronously, allowing programs to remain responsive while processing streams of information.
Sequential Processing: Each emitted value is processed through all intermediate operators before the next value is emitted, maintaining order and predictability.
Backpressure Handling: Unlike traditional callback approaches, flows automatically manage the speed mismatch between producers and consumers through suspension mechanisms.
Composability: Flow operators can be chained together to create complex data processing pipelines in a clean, readable manner.
kotlinlang.org/docs/flow,
proandroiddev.com/kotlin-flows-guide,
kt.academy/article/cc-understanding-flow
developer.android.com/kotlin/flow,
reactive-streams.org
Cold Flows vs. Hot Flows
Cold Flows (Flow)
Think of cold flows like a TV show that's recorded:
- Each time someone presses "play," the show starts from the beginning
- If two people watch at different times, they each see the full show from the start
- The show only plays when someone watches it
Use cold flows for:
- Fetching data from a database
- Getting information from a website
- Any operation where each user needs their own copy of the data
Example:
val myFlow: Flow<Int> = flow {
for (i in 1..5) {
delay(100) // Wait a little bit
emit(i) // Send the number
}
}
This creates a flow that sends numbers 1, 2, 3, 4, 5 (one every 100 milliseconds).
Hot Flows (StateFlow and SharedFlow)
Think of hot flows like a live TV broadcast:
- Everyone watches the same broadcast at the same time
- If you join late, you miss the earlier parts (unless it replays them)
- The broadcast keeps happening whether anyone is watching or not
StateFlow: Like a scoreboard that shows the current score
- It always has a current value (the scoreboard always shows the latest score)
- New viewers immediately see the current score
val score = MutableStateFlow<Int>(0) // Score starts at 0
SharedFlow: Like a radio station broadcasting events
- Multiple people can listen at the same time
- You can choose to replay recent announcements to people who just tuned in
val announcements = MutableSharedFlow<String>(replay = 1)
blog.jetbrains.com/kotlin/kotlinx-coroutines-stateflow-sharedflow,
kt.academy/article/cc-sharedflow-stateflow,
stackoverflow.com/questions/kotlin-flow-swift
Flow Operators and Transformations
Operators are like tools in a toolbox. They let you transform, filter, and combine streams:
Transformation operators: map, transform, flatMapConcat, flatMapMerge allow modifying emitted values
Filtering operators: filter, take, drop, distinctUntilChanged control which values pass through
Terminal operators: collect, toList, first, reduce consume the flow and trigger execution
Error handling: catch provides declarative exception handling without disrupting flow execution
map - Change each piece of data
flow { emit(1); emit(2); emit(3) }
.map { number -> number * 2 } // Each number gets doubled
// Results: 2, 4, 6
filter - Keep only certain data
flow { emit(1); emit(2); emit(3); emit(4) }
.filter { number -> number > 2 } // Only keep numbers bigger than 2
// Results: 3, 4
collect - Get the data and use it
myFlow.collect { value ->
println("Got value: $value") // Print each value as it arrives
}
You can chain operators together in commonMain to create complex data processing pipelines that work identically across all platforms. For example:
// Shared in commonMain
class NewsRepository {
val latestNews: Flow<List<Article>> =
remoteDataSource.fetchNews()
.map { articles -> articles.filter { it.isRecent } }
.flowOn(Dispatchers.IO)
.catch { exception -> emit(getCachedNews()) }
}
This hybrid approach lets you write business logic once while adapting presentation layers for each platform.
kotlinlang.org/docs/flow,
bugfender.com/blog/kotlin-flows,
proandroiddev.com/kotlin-flows-guide
Best Practices for Beginners
Start by using cold flows for most operations like API requests and database queries. They're simpler and restart independently for each collector, preventing shared state issues.
Use StateFlow for UI state management when you need to represent current state that should be immediately available to new observers. It automatically conflates values and replays the latest state.
Use SharedFlow for one-time events like navigation actions, showing toasts, or handling user clicks that shouldn't be replayed when the screen rotates.
Collect flows in lifecycle-aware ways on Android using repeatOnLifecycle(Lifecycle.State.STARTED) to prevent unnecessary work when UI is not visible. For iOS in KMP, wrap flows in callbacks or use libraries like SKIE that generate Swift-friendly APIs.
Test on all targets early to catch platform-specific issues. Flow logic in commonMain should work everywhere, but collection mechanisms differ between platforms.
For dependency management, add kotlinx-coroutines-core to commonMain in your version catalog, and the Kotlin plugin will automatically propagate it to platform-specific sets. This avoids duplicating declarations and ensures consistent behavior.
Start simple: Collect basic flows first, then progressively add operators like map, filter, and catch. Flows compose naturally, so you can build complex pipelines incrementally.
Common Mistakes to Avoid
Don't forget to collect - A flow does nothing until you call collect(). It's like having a water hose connected but never turning it on.
Don't mix up StateFlow and Flow - Use StateFlow when you need the current value always available. Use Flow for sequences of events.
Don't create flows inside loops - This wastes resources. Create one flow and reuse it.
developer.android.com/kotlin/flow,
jetbrains.com/help/kotlin-multiplatform-dev
Special Features in KMP
A key feature that makes flows special in KMP is their automatic backpressure management. Unlike RxJava, which requires separate Flowable types for backpressure support, Kotlin Flow handles this automatically through suspension. When a consumer is slow, the producer suspends until the consumer is ready, preventing buffer overflow and this works consistently across all platforms.
Context preservation ensures flows maintain the coroutine context throughout the pipeline. The flowOn operator lets you switch execution contexts (like moving network operations to an IO dispatcher) without leaking threads downstream, which is crucial for performance on resource-constrained mobile devices.
Structured concurrency integration means flows automatically cancel when their parent scope is cancelled. This prevents memory leaks and ensures predictable lifecycle management essential for mobile apps where components can be destroyed at any time.
Version catalogs can manage flow-related dependencies centrally. Define kotlinx-coroutines-core once in libs.versions.toml, then reference it across all source sets with type-safe accessors. This simplifies handling multiplatform setups and ensures consistent versions across targets.
