A comprehensive guide to understand Kotlin Flows

Ishan Vohra
ProAndroidDev
Published in
5 min readMar 1, 2024

--

Kotlin introduced Flow API a while back in version 1.3.0 in its coroutines library. The goal was simple with this one, to simplify asynchronous programming and stream processing in the Kotlin language. But how does it help us as developers? Let’s dive into the Flow API to understand how it works and how we can use it efficiently.

Kotlin introduced Flow API a while back in version 1.3.0 in its coroutines library. The goal was simple with this one, to simplify asynchronous programming and stream processing in the Kotlin language. But how does it help us as developers? Let’s take a dive into the Flow API to understand how it works and how we can use it efficiently.

What is Flow?

The official documentation says that Flow is:

An asynchronous data stream that sequentially emits values and completes normally or with an exception.

Simply, a flow is a stream of data with two ends, namely, a producer and a collector. One produces or emits data into the stream, and one collects the same. This is defined as a cold flow.

Two types of operations can be performed on a Flow:

  1. Intermediate operations: These include functions, called Intermediate operators, that are applied to the upstream flow or flows and return a downstream flow where further operations can be applied to.
    Upstream? Downstream? Confusing right?
    To simplify the above statement, Intermediate operators enable us to perform various tasks on the data that is being emitted. Below are some examples:
    a. map()function: Returns a flow containing the results of applying the given transform function to each value of the original flow.
    b. filter() function: Returns a flow containing only values of the original flow that matches a particular condition.
    There are more such operators and if they don’t suffice, we can create our own by creating custom extension functions.
  2. Terminal operators: These include functions which are either suspended such as collect, single, reduce, toList, or launchIn, which starts the collection of the flow in a given coroutine scope. These operators are applied wherever we want the data from the stream to be collected. Calling these functions also executes all the intermediatory operators as well before finally getting the output.

Now that we know how what ‘flow’ is, let’s see an example and try to understand how it all works.

Creating a flow

There are two ways we can create a flow.

Using the AbstractFlow class

We can implement the AbstractFlow class and emit values in the collectSafely() function. Let’s take the below example:

Creating flow by implementing the AbstractFlow class

In the above snippet, we’re emitting integer values from a list with a delay of 1000 milliseconds (1 second). We can collect these values using the collect() function wherever we want to perform operations on the output.

Using the flow{…} builder function

An easier approach to create a flow is using the flow{…} builder function as shown in the snippet below. We do not need a custom class for this and is much easier to use.

Creating flow using flow{…} builder function

The output for the main() function in both cases will be the same (shown below).

Output

The integer values are collected one by one after 1 second delay between two consecutive values.

Using intermediatory operators

What if we’re interested in only the odd integers coming out of the above defined flow.

We can use a chain of certain functions before we call collect(). Let’s use one such function called filter() and add a condition to only collect the odd values.

Collecting a flow with a filter

The output of the above code will be:

To be clear, the flow{…} builder function we defined earlier did actually emit the even numbers as well, but the filter() function discarded those values before being collected.

One interesting property of intermediatory property is that we can chain multiple of these functions (as many as we want) before the collect() call which enables us to get the output in any format we want.

Let’s see an example for this. What if we had a recurring item in the integers list and we wouldn’t want to collect same value again and again. We can use a function called distinctUntilChanged(), which prevents the collect() to get values which is equal to what it got the last time. Check the snippet below:

We can see that the integer value 3, is consecutively repeating but with the distinctUntilChanged() , we’ll only get one of them. Let’s run the above code to verify.

And there it is. 3 is collected only once.

We have understood at this point on how we can create and use the values emitted in it. There are situations where in our Kotlin code or project, we need to keep a check on values that keep changing and this is a great way to do it.

But are there any benefits to using flows in your project?

Benefits of using flows

  1. Async process: Flow allows you to manage asynchronous operations in a non-blocking and sequential manner, eliminating the need for callbacks leading to a more readable and manageable code.
  2. Backpressure Management: Flow inherently handles backpressure situations, preventing the producer from overwhelming the consumer with data faster than it can be processed. This eliminates the need for manual backpressure handling, a common challenge in asynchronous programming.
  3. Nullability Support: Flow inherits Kotlin’s strong null-safety features, ensuring type safety and reducing the risk of null pointer exceptions in your code.
  4. Coroutines Integration: Flow builds upon coroutines, a built-in Kotlin feature for structured concurrency. This tight integration simplifies flow usage within existing coroutine-based code and leverages coroutines’ benefits like lightweight threads and cancellation capabilities.

Conclusion

And with this, we can conclude this introductory article on Kotlin flows. It’s an addition to the language which should be welcomed since it makes the language more robust and independent in handling scenarios where the code needs to handle a stream of data. There are many implementations of flow such as StateFlow, SharedFlow, etc which can be useful in specific situations and if none of the pre-made implementations suffice to your needs, creating a new custom implementation is quite simple as well.

Thank you for reading! If you’re interested in making the search feature in your app or website much faster. Read this article on debouncing and how can it elevate your app/website’s performance.

--

--