Kotlin Flows

Anirudh Goud
6 min readMay 7, 2023

--

In this post let's understand the concept of flows in Kotlin and some powerful features it offers.

Let’s begin by breaking it down in steps to better grasp the concepts in detail.

What are Flows ?
Benefits of using Flows
How are Flows Constructed?
Intermediate Operators?
Terminal Operators?

What are Flows ?

Flows represent stream of values that are asynchronously computed. They are built on top of coroutines making it easy to combine them with other coroutine-based operations.

🙄🙄🙄 ?????

The definition I’m sure must not have gone well isn’t it ? Lets’s understand with an example then.

Assume we have a news channel that continuously gives out news at any point of time, so it is nothing but a continuous stream of data that is emitted and we keep listening to it and that my friend is a flow.

Even live audio and video streaming are also better examples that fall under this category

Consider a flow as values sent from one end and recieved on other end. The terminology we use in flows to send a value is Emit and recieve the sent value is Collect.

Where to use Flows ?

Now that from the above example you must have got an idea as to what flows are let’s see what use case can be the best suited for this purpose.

Flows are particularly useful in situations where you need to process large amounts of data asynchronously and non-blockingly such as in the following scenarios.

  • Network requests: When making network requests, flows can be used to handle the response data asynchronously and non-blockingly, allowing your application to remain responsive while the request is being processed.
  • Database operations: Flows can be used to process data from databases asynchronously and non-blockingly, allowing you to handle large amounts of data without blocking the main thread.

How are Flows Constructed ?

These are the following basic ways to create a flow and we also call then flow builders:

  • flow { … } : builder function to construct arbitrary flows from sequential calls to emit function.
  • flowOf(…) : functions to create a flow from a fixed set of values.
  • asFlow() : extension functions on various types to convert them into flows.
Basic flow {…} builder example

In the above example as you can see we have a function simpleFlow() that constructs a flow of Integers using the very basic flow builder flow {…}. Within that function two values are emitted with a delay of 1 second. The emitted values are collected/received using the collect suspend function.

similarly we can construct flows using other two approaches just as the code snippets below for the builders flowOf( ) and asFlow( )

fun flowOfBuilder(): Flow<Int> {
return flowOf(12, 15, 14) // The flowOf builder defines a flow that emits a fixed set of values.
}
fun asFlowBuilder(): Flow<Int> {
return (95..100).asFlow()// Convert any collect to or ranges using asFlow extension
}

Intermediate Operators

Intermediate operators in flows are operators that transform the emitted data in some way, but do not terminate the flow. They are also known as “transforming” operators, and are typically used to filter, map, or transform the data emitted by a flow.

Some examples of intermediate operators in flows include:

  1. map: Transforms each emitted value by applying a mapping function to it.
  2. filter: Filters the emitted values based on a given predicate function.
  3. take: Takes the first n values emitted by the flow.
  4. distinctUntilChanged: Filters out consecutive duplicate values emitted by the flow.

fun main() {
runBlocking {
// Map Operator Example
numbers().map {
delay(1000)
it + 2
}.collect {
println("Mapped values are $it")
}
}
}

fun numbers(): Flow<Int> = flow {
emit(1)
emit(2)
emit(3)
}

The above code snippet is an example of map operator. It maps values by adding 2 to each emitted item and then collects the transformed value in the collect operator. so the output will be 3,4,5.

    // Filter Operator Example
//------------------------------------------------------------------
numbers().filter {
delay(1000)
it % 2 == 0
}.collect {
println("$it") // prints 2
}

// The output of the above filter code will be 2 and the rest will be skipped as
// it doesn't satisfy the filter condition.

The above code snippet block on filter operator should be clear from the comments added at the end of the block.

Terminal Operators

Terminal operators on flows are suspending functions that start a collection of the flow. The collect operator is the most basic one, but there are other terminal operators as well.

Some examples of terminal operators in Kotlin flows

  1. collect: Collects all the emitted values of the flow and passes them to a lambda function for processing.
  2. toList: Collects all the emitted values of the flow and returns them as a list.
  3. toSet: Collects all the emitted values of the flow and returns them as a set.
  4. reduce: combines the emitted values of the flow using a lambda function and returns the result.
  5. fold: combines the emitted values of the flow using a lambda function and an initial value, and returns the result.
  6. single : It is used to consume the data emitted by a flow and return a single value if and only if the flow emits exactly one value. If the flow emits more than one value, or no value at all, an exception is thrown.
//function to emit flows with a second dealy
fun numbers(): Flow<Int> = flow {
emit(1)
delay(1000)
emit(2)
delay(1000)
emit(3)
}

//function to emit flows with a single value
fun singleValueFlow() = flowOf(123)

fun main() {
runBlocking {

1.// Collect Terminal operator
numbers().collect {println("collected values are $it")}


6. // Single Terminal operator
println(singleValueFlow().single())


2. // toList() Terminal operator
val list = numbers().toList()
println(list)


3. // toSet() Terminal operator
val set = numbers().toSet()
println(set)

}
}

The code snippet block above has code examples for Collect(), single(), toList(), toSet() operators and alos they are self explanatory from their description itself.

reduce and fold

These terminal Operators allow us to combine the emitted values into a single result.

val flow = flowOf(1, 2, 3, 4, 5)
val sum = flow.reduce { accumulator, value ->
accumulator + value
}

println(sum) // prints "15"

In this example, the flowOf function is used to create a flow that emits the values 1, 2, 3, 4, and 5. The reduce operator is then used to sum the values emitted by the flow, by applying the lambda function { accumulator, value -> accumulator + value } to each pair of emitted values in turn. The result is the sum of all the values in the flow, which is 15.

In this case the initial accumulator value is 0 and then when the first two values are computed it gets stored in accumulator. Now for every value emitted it gets added with accumulator until all values in the flow are processed thereby producing a final result.

fold

The fold operator is similar to reduce but with only one difference. This operator takes initial value argument as accumulator value.

val flow = flowOf(1, 2, 3, 4, 5)
val sum = flow.fold(5) { accumulator, value ->
accumulator + value
}

println(sum) // prints "20"

In the above code block i am providing a default value of 5 , this value is taken as an argument for accumulator and it keeps adding the emitted values to it and finally produces a result.

Thank you for taking time out to read my article. This is written entirely based on my understanding of the topic and i assume there could be other ways to achieve the same result. Feel free to write in your comments/questions. Appreciate some claps if you find this content useful.

Here’s the link to the App code on https://github.com/AnirudhPudari/KotlinFlows

You can connect with me on Linkedin!

--

--

Anirudh Goud

An Android Engineer. I'd love to share my learnings and this very process makes me learn more!!