Kotlin Help

Flow operators

Flow operators let you transform and process values in a flow pipeline. Kotlin provides two main kinds of flow operators:

  • Intermediate operators return a new downstream flow that consumes values from upstream flows and applies an operation to them.

  • Terminal operators trigger the execution of the flow pipeline by collecting an upstream flow. They may also return a result.

While the kotlinx.coroutines library offers a wide range of flow operators, you can also define custom ones when you need behavior that the built-in operators don't provide.

Intermediate operators

Intermediate operators return a new downstream flow that consumes values from upstream flows. You can chain several intermediate operators to build a flow pipeline before collecting the final result.

Intermediate operators can be classified by purpose into the following categories:

Transforming operators

Transforming operators transform the values emitted by an upstream flow. You can use them to convert values to another type, skip values, or emit additional values downstream.

The .transform() operator is a general transforming operator that you can use as the basis for more specific transforming operators, such as .map() and .filter().

Here's an example that uses the .transform() operator to emit each upstream value as many times as its value:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simplified custom implementation of the default .transform() operator inline fun <T, R> Flow<T>.myTransform( // Accepts a suspending lambda that can emit values downstream crossinline transform: suspend FlowCollector<R>.(value: T) -> Unit ): Flow<R> = flow { // Collects values from the upstream flow this@myTransform.collect { value -> // Applies the transformation and emits values to the downstream flow this@flow.transform(value) } } // Uses the default .transform() operator suspend fun main() = withContext(Dispatchers.Default) { val flow = (0..4).asFlow().transform { value -> // Emits each value as many times as its value repeat(value) { emit(value) } } println(flow.toList()) // [1, 2, 2, 3, 3, 3, 4, 4, 4, 4] }

You can use the .map() operator to transform each upstream value into one downstream value.

Here's an example that uses .map() to multiply each value by four:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simplified custom implementation of the default .map() operator inline fun <T, R> Flow<T>.myMap( crossinline transform: suspend (value: T) -> R ): Flow<R> = transform { value -> emit(transform(value)) } //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // Multiplies each upstream value by four val flow = (0..4).asFlow().map { it * 4 } println(flow.toList()) // [0, 4, 8, 12, 16] } //sampleEnd

To emit only upstream values that match a condition, use the .filter() operator.

Here's an example that emits values where dividing by 3 leaves a remainder of 1:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simplified custom implementation of the default .filter() operator inline fun <T> Flow<T>.myFilter( crossinline predicate: suspend (value: T) -> Boolean ): Flow<T> = transform { value -> // Emits only values that match the condition if (predicate(value)) emit(value) } //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // Emits only values where dividing by 3 leaves a remainder of 1 val flow = (0..10).asFlow().filter { it % 3 == 1 } println(flow.toList()) // [1, 4, 7, 10] } //sampleEnd

Some operators can combine the behavior of other transforming operators, such as .map() and .filter(), by transforming values and emitting only the results that match a condition.

For example, use .mapNotNull() to transform each upstream value and emit only non-null results:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simplified custom implementation of the default .mapNotNull() operator inline fun <T, R: Any> Flow<T>.myMapNotNull( crossinline transform: suspend (value: T) -> R? ): Flow<R> = transform { value -> transform(value)?.let { transformed -> emit(transformed) } } //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // Converts each string to Double and skips values that can't be converted val flow = flowOf("1.2", "10", "11", "error", "0.000") .mapNotNull { it.toDoubleOrNull() } println(flow.toList()) // [1.2, 10.0, 11.0, 0.0] } //sampleEnd

Filtering and size-limiting operators

Filtering and size-limiting operators control which values continue downstream from a flow. You can use them to remove repeated consecutive values, skip values from the beginning of a flow, or cancel collection after a specified number of values.

To ignore repeated consecutive values, use the .distinctUntilChanged() operator. It emits a value only when it differs from the previously emitted value:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simplified custom version of the default .distinctUntilChanged() operator fun <T> Flow<T>.myDistinctUntilChanged(): Flow<T> = flow { var lastEmitted: Any? = Any() // A value that's equal only to itself this@myDistinctUntilChanged.collect { value -> if (lastEmitted != value) { this@flow.emit(value) lastEmitted = value } } } suspend fun main() = withContext(Dispatchers.Default) { // Removes repeated consecutive values from the upstream flow val flow = flowOf(1, 2, 3, 3, 3, 4, 5, 5, 1).distinctUntilChanged() println(flow.toList()) // [1, 2, 3, 4, 5, 1] }

You can use the .drop() operator to skip the first values emitted by an upstream flow. For example, .drop(2) skips the first two values and emits the remaining values downstream:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simplified custom version of the default .drop() operator fun <T> Flow<T>.myDrop(count: Int): Flow<T> = flow { require(count >= 0) var elementsAlreadyDropped = 0 this@myDrop.collect { value -> if (elementsAlreadyDropped == count) { this@flow.emit(value) } else { ++elementsAlreadyDropped } } } //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // Skips the first two values from the upstream flow val flow = flowOf(1, 2, 3, 4, 5).drop(2) println(flow.toList()) // [3, 4, 5] } //sampleEnd

To cancel collection after a fixed number of values, use the .take() operator. Here's an example that uses the .take() operator to collect only the first three values:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.random.* import java.io.IOException import kotlin.time.Duration.Companion.milliseconds // A simplified custom version of the default .take() operator fun <T> Flow<T>.myTake(count: Int): Flow<T> = flow { require(count > 0) val cancellationException = CancellationException() var elementsRemaining = count try { this@myTake.collect { emit(it) --elementsRemaining if (elementsRemaining == 0) { // Cancels the upstream flow after the requested number of values throw cancellationException } } } catch (e: Throwable) { if (e === cancellationException) { // Handles the CancellationException used to cancel the upstream flow // Completes the flow after the set number of values in .myTake() } else { // Rethrows unexpected exceptions throw e } } } //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // Collects only the first three values from the upstream flow val flow = (0..1000).asFlow().take(3) println(flow.toList()) // [0, 1, 2] } //sampleEnd

Concurrent processing operators

By default, a flow pipeline processes values sequentially. The upstream flow emits a value, and collectors process it before the next value is emitted.

To run the upstream flow concurrently with downstream collection, use concurrent processing operators to introduce a buffer. A buffer stores values that the upstream flow has emitted, but the collector hasn't processed yet.

One operator that introduces this buffer is the .buffer() operator. It lets you configure the buffer capacity and what happens when the buffer is full, for example:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds //sampleStart suspend fun main() = withContext(Dispatchers.Default) { flow { repeat(10) { emit(it) println("Emitted $it!") } } // Lets the upstream flow emit up to four values ahead of the collector .buffer(4) .collect { println("Processed $it!") delay(20.milliseconds) } } //sampleEnd

When the collector is slower than the upstream flow, the pipeline needs a way to handle values that the collector hasn't processed yet.

By default, the collector applies backpressure to the upstream flow. With this strategy, the upstream flow suspends when the buffer is full and resumes when the collector frees up space.

To drop values instead of suspending the upstream flow, set the onBufferOverflow parameter:

import kotlinx.coroutines.* import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds //sampleStart suspend fun main() = withContext(Dispatchers.Default) { flow { repeat(10) { emit(it) println("Emitted $it!") } } // Stores up to four values before applying the overflow behavior // Drops the oldest buffered value when the buffer is full .buffer(4, onBufferOverflow = BufferOverflow.DROP_OLDEST) .collect { value -> println("Processed $value!") delay(20.milliseconds) } } //sampleEnd

You can also use the .conflate() operator, which is a shorthand for buffer(1, onBufferOverflow = BufferOverflow.DROP_OLDEST). Use it when you only want to process the latest values, skipping the values emitted while the previous value is being collected:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds //sampleStart suspend fun main() = withContext(Dispatchers.Default) { flow { repeat(10) { emit(it) println("Emitted $it!") } }.conflate().collect { println("Processed $it!") delay(20.milliseconds) } } //sampleEnd

The .conflate() operator only affects which buffered values the collector processes. It doesn't cancel processing that has already started. To do that, use collectLatest() instead.

In the previous examples, the .buffer() and .conflate() operators run the upstream flow concurrently in a separate coroutine without changing its coroutine context.

To run the upstream flow in a different coroutine context, use the .flowOn() operator. If this changes the dispatcher, .flowOn() collects the upstream flow in a separate coroutine and uses a buffer between upstream emission and downstream collection.

Here's a simplified example that uses .flowOn() to run the upstream flow in Dispatchers.IO:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds //sampleStart suspend fun main() = withContext(Dispatchers.Default) { flow { repeat(10) { emit(it) } println("Finished emitting!") }.flowOn(Dispatchers.IO).collect { println("Received $it!") delay(10.milliseconds) } } //sampleEnd

In this example, the .flowOn() operator can introduce concurrent upstream processing, but the buffer behavior isn't configured explicitly.

To configure both the coroutine context of the upstream flow and the buffer behavior, combine .flowOn() with .buffer() or .conflate(). When you use these operators together, the operators perform operator fusion and share a single buffer.

Here's an example that uses .flowOn(Dispatchers.IO) to run the upstream flow in Dispatchers.IO and .conflate() to keep the newest buffered value:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds import kotlin.math.round //sampleStart fun awaitSensorSignal(): SensorSignal { Thread.sleep(10) val reading = round(Random.nextDouble(25.0, 100.0) * 100.0)/100.0 println("Measured $reading as the temperature") return SensorSignal(temperatureCelsius = reading) } data class SensorSignal( val temperatureCelsius: Double ) suspend fun sendLatestTemperature(temperatureCelsius: Double) { println("Starting to send $temperatureCelsius...") delay(50.milliseconds) println("Sent $temperatureCelsius.") } suspend fun main() = withContext(Dispatchers.Default) { val smartHomeTemperatureFlow = flow { while (true) { val signal = awaitSensorSignal() emit(signal.temperatureCelsius) println("Emitted $signal") } } // Runs the upstream flow in Dispatchers.IO .flowOn(Dispatchers.IO) // Keeps the newest buffered value and drops older ones .conflate() // Collects the first two values from the upstream flow .take(2) .collect { temperature -> println("Received $temperature!") sendLatestTemperature(temperature) } } //sampleEnd

Combining operators

Combining operators consume values from multiple upstream flows and return a single downstream flow. Use them when collectors need values from more than one flow.

To pair values from two upstream flows, use the .zip() operator. It combines the first value from each flow, then the second value from each flow, and so on. The resulting flow completes as soon as one of the upstream flows completes.

Here's an example:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.random.Random import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource //sampleStart suspend fun main() = withContext(Dispatchers.Default) { // Emits a ticker value every 100 milliseconds val tickerFlow = flow { while (true) { emit(Unit) delay(100.milliseconds) } } val start = TimeSource.Monotonic.markNow() tickerFlow // Combines each ticker emission with the next number .zip(flowOf(1, 2, 3)) { _, value -> value }.collect { println("${start.elapsedNow()}: received $it") } } //sampleEnd

To combine the latest value from multiple flows, use the .combine() operator. It emits a new value when any upstream flow emits a value, using the latest value from each upstream flow:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource //sampleStart enum class Theme { Dark, Light, } data class UiState( val messages: List<String>, val theme: Theme, ) val messagesFlow = MutableStateFlow( listOf( "Hello!", "Is anyone here?", ) ) val themeFlow = MutableStateFlow( Theme.Light ) // Combines the latest values from both upstream flows val uiStateFlow = combine(messagesFlow, themeFlow) { messages, theme -> UiState(messages, theme) } suspend fun main() { withContext(Dispatchers.Default) { // Uses UNDISPATCHED to subscribe before the first update happens val uiUpdateJob = launch(start = CoroutineStart.UNDISPATCHED) { uiStateFlow.collect { // Draws the UI println(it) } } messagesFlow.update { messages -> messages + "I'll be back!" } delay(100.milliseconds) themeFlow.value = Theme.Dark delay(100.milliseconds) uiUpdateJob.cancel() } } //sampleEnd

In this example, combine() creates uiStateFlow from the latest values of messagesFlow and themeFlow. Updating either upstream flow emits a new UiState with the latest messages and theme.

If you want to collect values from multiple flows concurrently and emit their values into one downstream flow, use the .merge() operator:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource //sampleStart interface UiEvent class ClickEvent: UiEvent class RightClickEvent: UiEvent suspend fun main() { withContext(Dispatchers.Default) { val clickFlow = MutableSharedFlow<ClickEvent>() val rightClickFlow = MutableSharedFlow<RightClickEvent>() coroutineScope { // Uses UNDISPATCHED to subscribe before the first update happens val collectJob = launch(start = CoroutineStart.UNDISPATCHED) { // Collects both upstream flows concurrently and emits their values downstream merge(clickFlow, rightClickFlow).collect { println("Observed an event: $it") } } clickFlow.emit(ClickEvent()) delay(100.milliseconds) clickFlow.emit(ClickEvent()) delay(100.milliseconds) rightClickFlow.emit(RightClickEvent()) delay(100.milliseconds) collectJob.cancel() } } } //sampleEnd

Lifecycle operators

Lifecycle operators accept a suspending lambda that runs at a specific point during flow collection. You can use them to place logic before the flow is collected, before each value is emitted, after collection completes, or when the flow completes without emitting values.

The .onStart() operator runs its lambda before the upstream flow is collected. For code that needs to run before each emitted value, use .onEach().

Here's an example that uses these operators to print a message before collection starts and before each value is emitted downstream:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource // A simplified custom version of the default .onStart() operator fun <T> Flow<T>.myOnStart( action: suspend FlowCollector<T>.() -> Unit ): Flow<T> = flow { this@flow.action() this@myOnStart.collect(this@flow) } suspend fun main() { withContext(Dispatchers.Default) { flowOf("Page 1", "Page 2", "Page 3").onStart { println("Processing pages!") }.onEach { println("Emitted $it") }.collect { println("Collected $it") } } }

To run code after collection completes, use the .onCompletion() operator. Its lambda can emit values downstream when the upstream flow completes successfully, for example:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource // A simplified custom version of the default .onCompletion() operator fun <T> Flow<T>.myOnCompletion( action: suspend FlowCollector<T>.(cause: Throwable?) -> Unit ): Flow<T> = flow { var exception: Throwable? = null try { this@myOnCompletion.collect(this@flow) } catch (e: Throwable) { // Run `action`, but if `action` calls `emit`, throw `e` from it FlowCollector<T> { throw e }.action(e) throw e } this@flow.action(null) } //sampleStart suspend fun main() { withContext(Dispatchers.Default) { flowOf("Page 1", "Page 2", "Page 3").onCompletion { println("Almost done...") // Emits an additional value after the upstream flow completes emit("Last Page!") }.collect { println("Collected $it") } } } //sampleEnd

To run code when the upstream flow completes without emitting any values, use the .onEmpty() operator:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource // A simplified custom version of the default .onEmpty() operator fun <T> Flow<T>.myOnEmpty( action: suspend FlowCollector<T>.() -> Unit ): Flow<T> = flow { var emittedSomething = false this@myOnEmpty.collect { value -> emittedSomething = true this@flow.emit(value) } if (!emittedSomething) { action() } } //sampleStart suspend fun main() { withContext(Dispatchers.Default) { flowOf("Page 1", "Page 2", "Page 3").onEmpty { // Doesn't print anything, because the upstream flow emits values println("No pages to load!") }.collect() flowOf<Int>().onEmpty { println("No pages to load!") // No pages to load! }.collect() } } //sampleEnd

Terminal operators

Terminal operators collect a flow. You can use them to consume emitted values, return a result based on the collected values, or collect a flow in a specific CoroutineScope.

To collect a flow, use the collect() operator. If you pass a lambda to collect(), it receives each emitted value:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart suspend fun main() { withContext(Dispatchers.Default) { flowOf(1, 2, 3).collect { println("Collected $it!") } } } //sampleEnd

You can also call collect() without a lambda:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart suspend fun main() { withContext(Dispatchers.Default) { flowOf(1, 2, 3).onEach { println("Collected $it!") }.collect() } } //sampleEnd

If you want to collect a flow but cancel unfinished work when a new value is emitted, use the collectLatest() operator:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource //sampleStart suspend fun main() { withContext(Dispatchers.Default) { flow { println("Emitting Page 1") emit("Page 1") delay(50.milliseconds) println("Emitting Page 2 in quick succession") emit("Page 2") delay(200.milliseconds) println("Emitting Page 3") emit("Page 3") }.flowOn(Dispatchers.IO).collectLatest { println("Starting to process $it!") try { delay(100.milliseconds) } catch (e: CancellationException) { println("Canceled processing $it.") throw e } println("Done processing!") } } } //sampleEnd

Some terminal operators collect a flow and return a result based on the collected values. For example, you can use the .first() operator to return the first emitted value and then cancel collection:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource suspend fun main() { withContext(Dispatchers.Default) { val firstValue = flowOf(1, 2, 3).first() println(firstValue) // 1 } }

You can collect emitted values into a collection with the .toList() or .toSet() operators:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* // A simplified custom implementation of the default .toList() operator suspend fun <T> Flow<T>.myToList(): List<T> = buildList { this@myToList.collect { value -> // Adds each emitted value to the resulting list add(value) } } //sampleStart suspend fun main() { withContext(Dispatchers.Default) { val list = flowOf(1, 2, 3).toList() println(list) // [1, 2, 3] val set = flowOf(1, 2, 2, 3).toSet() println(set) // [1, 2, 3] } } //sampleEnd

To combine emitted values into a single result, use the .reduce() or .fold() operators. The .fold() operator uses the value you provide as its starting value, and the .reduce() operator uses the first emitted value instead.

Here's an example:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* //sampleStart suspend fun main() { withContext(Dispatchers.Default) { // Uses the first emitted value as the starting value val reduced = flowOf(1, 2, 3).reduce { accumulator, value -> accumulator + value } // Starts with the provided starting value val folded = flowOf(1, 2, 3).fold(2) { accumulator, value -> accumulator + value } println(reduced) // 6 println(folded) // 8 } } //sampleEnd

Collect a flow in a specific CoroutineScope

When a screen or another long-lived object needs values from a flow, start the collector in that object's CoroutineScope. This ensures that canceling the object's CoroutineScope when the object is destroyed also cancels collection.

To collect a flow in a specific CoroutineScope, use the .launchIn() terminal operator. This operator returns the Job of the collecting coroutine.

Here's an example where a screen collects values from a StateFlow and stops the collecting coroutine when the screen closes:

import kotlinx.coroutines.* import kotlinx.coroutines.flow.* import kotlin.time.Duration.Companion.milliseconds import kotlin.time.TimeSource // A simplified custom version of the default .launchIn() operator fun <T> Flow<T>.myLaunchIn(scope: CoroutineScope): Job = scope.launch { this@myLaunchIn.collect() } //sampleStart data class Coordinate(val x: Int, val y: Int) class MyScreen(val scope: CoroutineScope) { private val _mousePosition = MutableStateFlow<Coordinate>(Coordinate(0, 0)) val mousePosition get() = _mousePosition.asStateFlow() init { // Starts collecting the StateFlow in the screen's CoroutineScope mousePosition.onEach { updateStatusBar() }.launchIn(scope) } fun moveMouse(newCoordinate: Coordinate) { _mousePosition.value = newCoordinate } private fun updateStatusBar() { println("Mouse is at ${_mousePosition.value}") } } suspend fun main() { withContext(Dispatchers.Default) { val childScope = CoroutineScope( currentCoroutineContext() + Job(currentCoroutineContext()[Job]) ) val screen = MyScreen(childScope) delay(100.milliseconds) screen.moveMouse(Coordinate(10, 15)) delay(100.milliseconds) screen.moveMouse(Coordinate(1, 3)) delay(100.milliseconds) childScope.cancel() } } //sampleEnd
28 July 2026