What Are Kotlin Flows? Explained with Examples

Summarize this article with:

Suspend functions return one value. But most real apps need a stream of data that keeps coming, whether it’s database updates, user events, or live network responses. That’s the problem Kotlin flows solve.

So what are Kotlin flows, exactly? They’re cold asynchronous data streams built on top of Kotlin coroutines, designed to emit multiple values sequentially without blocking the thread.

Google now recommends flows as the standard approach for reactive programming on Android. Over 60% of professional Android developers use Kotlin as their primary language, and Flow has replaced RxJava and LiveData in most modern codebases.

This guide covers how flows work under the hood, the difference between cold and hot streams like StateFlow and SharedFlow, intermediate and terminal operators, error handling, Android integration, and performance patterns that matter in production.

What Are Kotlin Flows

maxresdefault What Are Kotlin Flows? Explained with Examples

Kotlin Flow is a cold asynchronous data stream built on top of coroutines. It emits multiple values sequentially, one at a time, and nothing happens until a terminal operator like collect is called.

That last part is what trips people up at first. A Flow doesn’t “run” on its own. You define it, chain some operators, and it just sits there until something triggers collection. Every new collector gets a fresh execution of the entire pipeline.

Suspend functions return a single value. Flow returns many. That’s the core distinction. If you need one result from a network call, a suspend function is fine. But if you’re watching a database for changes, streaming sensor data, or reacting to UI events over time, you need something that keeps emitting.

Flow lives in the kotlinx.coroutines.flow package, part of the broader Kotlin coroutines library. According to Google’s Android documentation, over 50% of professional developers using coroutines reported increased productivity. Flow extends that same coroutine foundation into reactive data stream territory.

A basic example looks like this: you create a flow { } block, call emit() inside it to push values downstream, and then call collect somewhere else to consume them. The emission and collection both happen inside coroutine contexts, so thread management stays clean.

Why is Kotlin becoming the new Java?

Discover Kotlin statistics: Android adoption, multiplatform growth, developer satisfaction, and the modern language evolution from JetBrains.

Explore Kotlin Data →

Kotlin has been Google’s preferred language for Android development since 2017. JetBrains reports that 95% of the top 1,000 Android apps include some Kotlin code. Flow is a big reason why. It replaced a lot of the reactive plumbing that teams used to handle with RxJava or custom callback systems.

How Kotlin Flows Work Under the Hood

maxresdefault What Are Kotlin Flows? Explained with Examples

Flow execution follows a simple rule: everything runs in the coroutine context of the collector. When you call collect, the flow { } builder starts executing its block. Values pass through intermediate operators, then arrive at the collector’s lambda. All of it happens sequentially in that same coroutine.

There’s no parallel execution by default. If the flow { } block emits five items, each one completes its journey through the operator chain before the next one starts. This makes the mental model straightforward, but it also means a slow collector blocks the entire pipeline.

Context preservation is enforced automatically. You can’t emit values from a different coroutine context inside a basic flow { } builder. Trying to launch a new coroutine or switch dispatchers inside the builder throws an exception. This prevents a whole class of concurrency bugs that would be really hard to trace.

Backpressure handling happens through coroutine suspension. When a collector can’t keep up, the emitter just… suspends. No buffer overflow, no dropped items, no strategy to configure. The coroutine pauses until the collector is ready for the next value.

At least, that’s the default. You can change it. But the fact that the safe option is the default matters a lot for teams building production apps. Google reports that Android apps using Kotlin are 20% less likely to crash compared to those that don’t, and features like this are part of the reason.

Flow Builders

Four builders cover practically every use case you’ll run into.

flow { } is the general-purpose builder. You write imperative code, call emit() wherever you want, and the block suspends between emissions. Most custom flows start here.

flowOf() creates a flow from a fixed set of values. Think of it as listOf() but for reactive streams. Useful for testing and for providing default values.

.asFlow() converts existing collections and sequences into flows. Got a List you want to process reactively? Call list.asFlow().

channelFlow and callbackFlow handle cases where the basic builder isn’t enough. channelFlow lets you emit from multiple coroutines concurrently. callbackFlow bridges callback-based APIs (like location listeners or Firebase callbacks) into the flow world. These two are critical when you’re dealing with API integration patterns that weren’t designed for coroutines.

Cold Streams vs. Hot Streams in Kotlin

maxresdefault What Are Kotlin Flows? Explained with Examples

Cold and hot. Two words that explain most of the confusion around Kotlin’s reactive types.

A cold stream doesn’t produce anything until someone starts collecting. Each collector triggers a fresh execution. Call collect three times on the same flow, and the flow { } block runs three times independently.

A hot stream emits values regardless of whether anyone is listening. SharedFlow and StateFlow are hot. They exist independently of collectors and keep their state or replay buffer active.

FeatureCold FlowHot Flow (StateFlow/SharedFlow)
Starts whenCollector calls collectCreated and active immediately
Multiple collectorsEach gets independent executionAll share the same emission source
Use caseOne-shot data fetches, DB queriesUI state, shared event streams
MemoryNo state held between collectionsHolds current value or replay buffer

The most common mistake I’ve seen in production code? Treating a cold flow like a hot one. Someone creates a flow that hits the network, then collects it from two different places, and wonders why the API call fires twice. That’s cold stream behavior working exactly as designed.

Netflix and Duolingo both use Kotlin extensively in their mobile apps. At KotlinConf 2025, Duolingo shared how they adopted a KMP-first approach, and getting the cold-vs-hot distinction right was central to their reactive data layer architecture.

If you need a single source of truth for UI state, use StateFlow. If you need a shared event bus, use SharedFlow. If you need one-shot operations like a database query or network request, plain Flow is the right call.

Intermediate Operators for Kotlin Flows

maxresdefault What Are Kotlin Flows? Explained with Examples

Operators sit between emission and collection. They transform, filter, or combine data as it passes through the pipeline. And they’re cold too. Chaining .map { } or .filter { } onto a flow doesn’t execute anything. It returns a new Flow instance with the transformation baked in, waiting for a terminal operator to kick things off.

The most used operators are exactly what you’d expect from any reactive or functional library:

  • map transforms each emitted value
  • filter drops values that don’t match a predicate
  • transform gives full control, lets you emit zero, one, or multiple values per input
  • take and drop control how many values reach the collector
  • distinctUntilChanged removes consecutive duplicates
  • onEach adds side effects (logging, analytics) without changing the stream

One nice thing about Kotlin Flow compared to RxJava: the map operator is a suspend function. In RxJava, you needed flatMap for async transformations and map for synchronous ones. In Flow, map handles both. Fewer operators to memorize, fewer chances to pick the wrong one.

Combining Multiple Flows

Real apps rarely depend on a single data source. You’re pulling from a database, a network endpoint, and maybe user preferences all at once. Kotlin gives you several ways to merge these streams.

combine reacts to the latest value from each flow. When any source emits, the combine block re-runs with the most recent value from every flow. This is the go-to for UI state that depends on multiple data sources.

zip pairs emissions one-to-one. First from flow A matches with first from flow B, second with second, and so on. If one flow is faster, it waits.

merge interleaves emissions from multiple flows into a single stream, preserving the order they arrive. Good for aggregating events from different user interactions.

flatMapLatest is the one that saves your bacon in search-as-you-type scenarios. When a new value arrives from the upstream flow, it cancels the previous inner flow and starts a new one. No stale network responses arriving after the user has already typed something else. If you’re doing mobile application development and building any kind of search feature, you’ll use this constantly.

Terminal Operators and Flow Collection

maxresdefault What Are Kotlin Flows? Explained with Examples

Nothing happens in a flow until a terminal operator runs. They’re the trigger. The collect call, the toList() call, the first() call. Without one of these, your flow is just a blueprint.

collect is the most common terminal operator. It’s a suspend function, so it needs a coroutine scope. It suspends the calling coroutine until the flow completes or the scope gets cancelled. Pretty much every Android ViewModel that uses flows has a collect call somewhere.

But it’s not the only option:

  • toList() and toSet() gather all emitted values into a collection
  • first() grabs the first emission and cancels the rest
  • single() expects exactly one value and throws if there are zero or more than one
  • reduce and fold accumulate values into a single result

launchIn deserves a special mention. Instead of wrapping collect in a launch block, you can call flow.onEach { }.launchIn(scope). It reads cleaner when you’re setting up multiple flow collections in one place, which happens a lot in ViewModels during the app lifecycle initialization.

JetBrains’ Developer Ecosystem survey shows Kotlin Multiplatform usage jumped from 7% to 18% between 2024 and 2025. As more teams share business logic across platforms, terminal operators become the bridge between shared flow-based data layers and platform-specific UI code.

Error Handling in Kotlin Flows

maxresdefault What Are Kotlin Flows? Explained with Examples

Errors in flows follow a concept called exception transparency. The catch operator only intercepts exceptions from upstream, meaning operators and emissions that come before it in the chain. Anything downstream, including the collect lambda, is outside its reach.

This is different from how most developers expect error handling to work, and it catches people off guard. Took me a while to internalize it myself.

Here’s the practical breakdown:

For upstream errors: use the catch operator. Place it after any operator that might throw. Inside the catch block, you can emit fallback values, log the error, or rethrow if needed.

For downstream errors: wrap your collect call in a standard try/catch. The catch operator won’t help you here because the exception happens after the flow pipeline.

For cleanup logic: onCompletion runs when the flow finishes, whether it completed normally, was cancelled, or threw an exception. Similar to a finally block.

One rule that’s easy to break: don’t use try/catch inside the flow { } builder to swallow exceptions. It violates exception transparency and can create hard-to-debug state issues. The flow infrastructure expects exceptions to propagate. When you silently catch them inside the builder, downstream operators like catch and onCompletion never know something went wrong.

In the broader context of software development best practices, this pattern aligns with the principle of failing explicitly rather than hiding errors. Google’s own documentation on flows strongly discourages exception suppression inside builders.

If your flow calls external services or runs operations that might fail intermittently, the retry operator is worth knowing. You pass in a retry count and a predicate that decides whether each specific exception warrants a retry. Network timeouts? Retry. Authentication failures? Probably not. This kind of granular control is what makes flows practical for production software development.

StateFlow and SharedFlow

maxresdefault What Are Kotlin Flows? Explained with Examples

StateFlow and SharedFlow are Kotlin’s built-in hot flow implementations. Unlike regular cold flows, these keep emitting regardless of whether anyone is collecting.

They exist to solve two specific problems that cold flows can’t handle: holding shared state and broadcasting events to multiple subscribers.

FeatureStateFlowSharedFlow
Initial valueRequiredNot required
Duplicate emissionsFiltered (distinctUntilChanged)All values emitted
Best forUI state in ViewModelsOne-shot events, broadcasts
Value access.value property availableNo current value property

StateFlow holds a single current value and only emits when that value actually changes. Set it to “Loading” three times in a row, and collectors only see it once. On Android, StateFlow is the modern replacement for LiveData in most Compose-based projects.

Google’s own documentation calls StateFlow a “great fit for classes that need to maintain an observable mutable state.” The Google Home team saw a 33% reduction in NullPointerException crashes after migrating to Kotlin, and StateFlow’s mandatory initial value (no nulls sneaking in) is part of that story.

SharedFlow is the more flexible sibling. No initial value, no deduplication. Every emission reaches every collector. Perfect for navigation events, snackbar triggers, or anything where you need “fire and forget” delivery to multiple listeners.

Converting a cold flow to either hot type is straightforward. Use stateIn to get a StateFlow, shareIn to get a SharedFlow. Both take a coroutine scope and a sharing strategy.

Replay and Buffer Configuration in SharedFlow

SharedFlow’s constructor takes three parameters that control its buffering behavior. Getting these wrong is one of the most common sources of dropped events in Android apps.

replay: how many past values new collectors receive immediately. Set to 0 for events (no replay). Set to 1 or more for state-like behavior.

extraBufferCapacity: additional buffer slots beyond replay. Helps when producers are faster than consumers.

onBufferOverflow: what happens when the buffer is full. Options are SUSPEND (default), DROPOLDEST, or DROPLATEST.

Kotlin Flows in Android Development

maxresdefault What Are Kotlin Flows? Explained with Examples

Flows are now at the center of how modern Android apps handle data. From database queries to network responses to UI state management, flow-based patterns show up everywhere in the software development process for Android.

Google reports that over 60% of professional Android developers use Kotlin as their primary language. The Android team’s recommendation is clear: use StateFlow for UI state, collect with lifecycle awareness, and let Room return flows directly from DAO queries.

ViewModel collection happens inside viewModelScope. You launch a coroutine, collect a flow from your repository, and push results into a MutableStateFlow. The scope handles cancellation automatically when the ViewModel is cleared.

Lifecycle-safe collection in Activities and Fragments uses repeatOnLifecycle. This API starts collection when the lifecycle hits STARTED and cancels it at STOPPED. Without it, you risk processing flow emissions while the app is in the background, which wastes resources and can cause crashes.

The Room persistence library returns Flow> directly from DAO methods. Any time the underlying table changes, Room automatically re-emits the updated query results. No manual refresh, no polling.

Ktor and Retrofit both support coroutine-based network calls that feed into flow pipelines. The typical pattern: a repository exposes a flow, the ViewModel collects it into StateFlow, and the Compose UI observes it with collectAsStateWithLifecycle().

Companies like Netflix, Cash App, and Philips have adopted Kotlin Multiplatform with flow-based architectures. At KotlinConf 2025, Google Workspace shared how they use shared Kotlin modules (including flow-based data layers) across Android, iOS, and web, with Room now offering Kotlin Multiplatform support.

Flows vs. RxJava and Other Reactive Alternatives

maxresdefault What Are Kotlin Flows? Explained with Examples

RxJava dominated Android’s reactive programming for years. But the migration to Kotlin Flow has been steady, and at this point, most new Android projects start with flows by default.

The biggest difference isn’t performance. It’s complexity.

RxJava has five stream types: Observable, Flowable, Single, Maybe, and Completable. Kotlin covers the same ground with two: suspend fun and Flow. Fewer types means fewer decisions, fewer mistakes, and a lot less cognitive load when reading someone else’s code.

AspectKotlin FlowRxJava
Stream types2 (suspend fun, Flow)5 (Observable, Flowable, etc.)
BackpressureBuilt-in via suspensionRequires Flowable + strategy
CancellationStructured concurrencyCompositeDisposable
Platform supportMultiplatform (Android, iOS, JVM, JS)JVM only
Operator styleSuspend functionsSync/async variants needed

According to the Reactive Scrabble benchmark (maintained in the kotlinx.coroutines repository), optimized Flow runs nearly twice as fast as optimized RxJava. Flow clocked 13.9 ms/op versus RxJava’s 23.6 ms/op on equivalent workloads.

Operator naming overlaps heavily. map, filter, flatMap, zip, combine. If you know RxJava, the learning curve for Flow operators is short. The key behavioral difference: Flow’s map accepts a suspend function, so you don’t need separate operators for synchronous and asynchronous transformations.

Trendyol’s Android team documented their RxJava-to-Flow migration in 2024, taking an incremental, module-by-module approach using the kotlinx-coroutines-rx2 bridge library. The interop layer lets you convert between Flow and RxJava types, so you don’t have to rewrite everything at once.

Sequences vs. Flow: Kotlin sequences are synchronous. They process items lazily but block the calling thread. Flows are asynchronous. If your data pipeline involves network calls, database reads, or any suspension point, Flow is the right choice. Sequences are fine for in-memory collection processing.

The decision of when to choose between Kotlin or Java for Android work often comes down to whether you want access to coroutines and flows. Java doesn’t have a direct equivalent to structured concurrency, and RxJava’s maintenance pace has slowed compared to the kotlinx.coroutines library.

Performance Considerations and Common Mistakes

Flows work well out of the box. But “works” and “works efficiently” aren’t the same thing, especially in apps with heavy data streams or complex UI state.

Here are the operators and patterns that matter most for production performance.

Decoupling Producers and Consumers

buffer() runs the collector in a separate coroutine from the emitter. Without it, a slow collector blocks the entire pipeline because flow is sequential by default.

conflate() goes further. It drops intermediate values entirely if the collector can’t keep up, always delivering the most recent emission. Good for UI updates where only the latest state matters.

JetBrains’ kotlinx.coroutines documentation sets DEFAULT_CONCURRENCY at 16 for operators like flatMapMerge. This default works for most apps, but high-throughput data processing may need tuning.

Dispatcher Management with flowOn

The flowOn operator shifts upstream execution to a different back-end dispatcher. Database reads should run on Dispatchers.IO. Heavy computation goes to Dispatchers.Default. Collection in Android usually stays on Dispatchers.Main.

Place flowOn as close to the data source as possible. Putting it right before collect shifts everything upstream, which might include operators that are perfectly fine on the main thread.

Structured Concurrency Pitfalls

Leaking collectors is the number one performance bug with flows. If you launch a coroutine that collects a flow but don’t scope it properly, that collection keeps running even after the screen is gone.

Android’s repeatOnLifecycle exists specifically for this reason. Without it, a flow collection launched in lifecycleScope.launch continues through onStop and keeps consuming resources in the background.

Another common mistake: misconfiguring stateIn with SharingStarted.WhileSubscribed(). The timeout parameter controls how long the upstream stays active after the last collector disappears. Set it too low, and the flow restarts on every configuration change (screen rotation). Set it too high, and you’re wasting resources. Google’s Android team recommends 5000 milliseconds as a reasonable default.

If you’re working within a team environment, having a code review process that specifically checks for unscoped flow collections and missing lifecycle awareness can prevent these issues from reaching production. The same applies to unit testing flow emissions. The Turbine library by Cash App makes flow testing straightforward, and it’s become the standard tool for verifying flow behavior in Kotlin projects.

FAQ on What Are Kotlin Flows

What is a Kotlin Flow in simple terms?

A Kotlin Flow is a cold asynchronous data stream built on coroutines. It emits multiple values sequentially over time, unlike suspend functions that return only one result. Nothing executes until a collector starts consuming the values.

What is the difference between Flow and StateFlow?

Flow is cold and starts fresh for each collector. StateFlow is hot, holds a current value, and shares emissions across multiple collectors. StateFlow always requires an initial value and filters consecutive duplicates automatically.

When should I use Kotlin Flow instead of LiveData?

Use Flow when you need operations beyond the main thread, want access to transformation operators like map and filter, or share logic across platforms with Kotlin Multiplatform. LiveData is limited to the Android UI layer.

How does backpressure work in Kotlin Flow?

Flow handles backpressure through coroutine suspension. When a collector is slower than the emitter, the emitter simply pauses until the collector is ready. No explicit strategy or separate stream type like RxJava’s Flowable is needed.

Can I use Kotlin Flow with Jetpack Compose?

Yes. Collect flows in your ViewModel using viewModelScope, expose them as StateFlow, then observe in Compose with collectAsStateWithLifecycle(). This pattern handles lifecycle awareness and configuration changes automatically.

What are the main flow builders in Kotlin?

Four builders cover most cases. flow { } for custom emission logic, flowOf() for fixed value sets, .asFlow() for converting collections, and callbackFlow for bridging callback-based APIs into the coroutine world.

Is Kotlin Flow faster than RxJava?

According to the Reactive Scrabble benchmark, optimized Flow runs at roughly 14 ms/op compared to RxJava’s 24 ms/op. Flow also uses less memory because it doesn’t create intermediate observable objects for each operator in the chain.

How do I handle errors in Kotlin Flow?

Use the catch operator for upstream exceptions and try/catch around collect for downstream errors. The onCompletion operator runs cleanup logic regardless of whether the flow completed normally or failed.

What is the difference between SharedFlow and StateFlow?

StateFlow holds one value, requires an initial state, and skips duplicate emissions. SharedFlow has no initial value, emits every value including duplicates, and supports configurable replay and buffer settings. Use StateFlow for state, SharedFlow for events.

Should I migrate from RxJava to Kotlin Flow?

For new Kotlin projects, yes. Flow integrates natively with coroutines, has fewer stream types to manage, and receives active support from JetBrains and Google. The kotlinx-coroutines-rx2 bridge library lets you migrate incrementally.

Conclusion

Understanding what are Kotlin flows gives you a practical foundation for building reactive, asynchronous data pipelines in modern Android and multiplatform projects. They’re not a nice-to-have anymore. They’re the standard.

Cold streams handle one-shot operations like network requests and database queries. StateFlow and SharedFlow cover shared UI state and event broadcasting. The operator chain, from map and filter to combine and flatMapLatest, keeps data transformations clean and readable.

Error handling through the catch operator, lifecycle-safe collection with repeatOnLifecycle, and performance tuning with buffer and conflate round out the toolkit.

Whether you’re migrating from RxJava or starting fresh with kotlinx.coroutines, flows fit naturally into structured concurrency. The learning curve is shorter than most reactive alternatives, and the JetBrains and Google ecosystem keeps expanding support.

Start with a simple flow builder, collect it in a ViewModel, and build from there.

50218a090dd169a5399b03ee399b27df17d94bb940d98ae3f8daff6c978743c5?s=250&d=mm&r=g What Are Kotlin Flows? Explained with Examples
Related Posts
Read More

How To Work With Maps In Kotlin

Summarize this article with: ChatGPT Claude Perplexity Grok Kotlin maps are everywhere in Android development. Caches, config objects,…