Walkthrough Kotlin Flows and Channels with Message Bubbles

Onur Emre Karadag
Daresay
Published in
9 min readAug 21, 2023

--

Understanding Kotlin Flows and Channels’ behaviors step by step with a message bubble example.

Almost two years ago, I faced a new challenge regarding socket communication. Until that time, I had some gaps in my knowledge about the reactive approach in the Kotlin world. I had been planning to improve my understanding, but then I found myself dealing with a new challenge that required me to use the reactive Kotlin approach extensively.

The feature I needed to implement was relatively straightforward: I had to connect to socket.io server with client API and display message bubbles every time I received a new message from the server. However, things became more complicated when I realized I also needed to listen these socket emits/new messages even if the app was in the background. I couldn’t update the UI when the view was in the background because it could lead to resource wastage and potentially cause of app crashes.

In the end, I had to discover a way to display every message in the UI using a lifecycle-aware method. When the app was in the background, I also needed to stack every new message until the user returned to the app.

When I was trying to understand what I needed to use for this challenge, seeing different scenarios in the process really helped me to grasp Kotlin Flows, Channels, Shared/State Flows, and the repeatOnLifecycle API. I thought that presenting this as a walkthrough could be helpful for anyone who struggles with the reactive Kotlin world. We will try to find the right solution together in this blog post. Let’s dive in!

Follow the steps with starter app!

If you’d like to follow along as we search for the right solution for our feature, a starter/main app is available in this repository: Bubbles. Feel free to write your code at every step, or switch between branches to see the final results.

The app is quite simple, consisting of one Activity, one ViewModel, XML with ViewBinding, and one RecyclerView with its custom adapter. Our focus will be on Kotlin Flows, Channels, and Shared/State Flows, so I wouldn’t recommend using other parts of the code as a reference. Upon successfully launching the app, you will see the following interface:

Bubbles Github

The starter code of the app simply loops from 1 to 10, delaying one second at every step before adding the item to the RecyclerView.

Forward to line

Let’s go with the Flow

We’re going to act like we’re real newbies to reactive programming in Kotlin. We’ve heard there’s something called Flow. Maybe it can solve our problem. Let’s give it a try. First, we need to add our Flow to the ViewModel:

Forward to line

Our Flow is emitting numbers from 1 to 10 for us. Now we can collect it in our Activity to update the UI. Flows are part of Kotlin Coroutines, so we need to collect its values with Coroutine Scope.

And this is the final result:

Flow

Everything looks cool, right? But wait a minute, what happens if our activity goes into the background?

Flow when app goes to background

Even if we keep our app in the background, the Flow continues to emit values, and we are trying to update our UI. You’ll want to collect these flows and update the UI without wasting resources or leaking data when the view goes into the background.

What about repeatOnLifecycle API?

With the help of the repeatOnLifecyle API, you can collect Flows in the UI layer without negative side effects. You can read details about the repeatOnLifecycle API from this excellent blog post. But basically, it provides a way to restart or repeat a given block of code within a lifecycle-aware component, ensuring that the code is only active when the component is in a specific state, such as STARTED or RESUMED, thereby improving resource management and reducing potential bugs.

Source for image

This is our code snippet for collecting Flow in the activity after adding the repeatOnLifecycle API:

And this is the result:

Flow with repeatOnLifecycle API

As you can see, we added repeatOnLifecycle with the STARTED state. Yes, our Flow stops emitting when we push the app to the background, but after bringing the app to the foreground, our Flow starts from the beginning to count.

This is happening because repeatOnLifecycle restarts its coroutine from scratch on each repeat and, cancels it each time the lifecycle falls below the specified state. Because of the flow’s cold stream behavior, our flow is not emitting anything when nobody collects it. But when we return to our app, the repeatOnLifecycle API causes the collect our flow again, and our ‘for’ loop starts from the beginning.

Flow with repeatOnLifecyleApi

You might suggest using different states for repeatOnLifecycle, but we will end up either not calling the flow again or creating it from scratch in every state. However, I highly recommend you play around in the Bubbles/Flow branch. Bubbles/Flow branch.

Making our Flows hot’er

We said that Flow is a cold stream component, and whenever nobody collects anything from it, it doesn’t emit values to us. The solution to this is using hot stream components. We can achieve this by using StateFlow or SharedFlow. These two components are the hot stream versions of Flows.

Note: You can convert cold stream Flows into hot stream StateFlow with the .stateIn() operator or SharedFlow with the .shareIn() operator, but these result in the same effects as using StateFlow and SharedFlow mostly. You can also change the emitting behavior of flows with these operators. However, we will not go into the details of them in this blog post.

Maybe its StateFlow?

The repeatOnLifecycle API seemed like it was almost going to solve our problem, but it couldn’t help us on its own. StateFlow is one of the solutions in hot stream flows.

A StateFlow requires an initial value upon construction and promptly emits this value to any observer as soon as the collection begins. It could also be the last value that was emitted. StateFlow runs on top of SharedFlow. Let’s give it a try.

This is how we create a StateFlow:

And this is how we collect StateFlow with repeatOnLifecycle API included.

We created one private MutableStateFlow to emit values and one StateFlow to collect them from outside of MainViewModel. Other than that, everything is quite the same, and this is the result:

StateFlow with repeatOnLifecycle API

Our StateFlow keeps emitting the values even when there is no collector. But when we return to the application, we can’t maintain the order as we wanted. And after the counting ends, StateFlow emits the last value again and again. This behavior might be useful for some cases like UI States, but it didn’t help us. You can play around with StateFlow in the Bubbles/stateflow branch.

The common mistake I’ve been seeing is showing dialogs, toast messages etc. with StateFlow when using the repeatOnLifecycleApi. You need to be more careful when displaying dialogs, especially when the user comes back from the background.

Then it needs to be SharedFlow, right?

We’ve talked about StateFlow’s special features, and we’ve mentioned that StateFlow stays under SharedFlow. StateFlow is designed to represent a single state value, emitting the current state to any new collectors, and only the latest value is retained.

This makes StateFlow suitable for representing UI state that changes over time. On the other hand, SharedFlow is a more general-purpose hot flow that allows multiple collectors and can represent a series of values over time. It also provides more control over buffering and replaying, accommodating more complex use-cases beyond simply holding a state. But first, let’s see it in its vanilla form.

This is how we create SharedFlow:

And this is how we collect SharedFlow with repeatOnLifecycle API included:

For now everything seems very similar. This is the result of it:

SharedFlow with repeatOnLifecycle API

Unlike StateFlow, SharedFlow does not repeat the last value and doesn’t require an initial value. However, we didn’t achieve the result we wanted because we are missing some values that were emitted when we were in the background.

Can we use buffering and replaying?

As we discussed earlier, SharedFlow’s most important feature is greater control over buffering and replaying. Buffering in SharedFlow refers to the ability to temporarily store a certain number of emitted values before they are collected. This can be useful if the producer is emitting values faster than the consumers are collecting them. You can specify the buffer size using the extraBufferCapacity parameter when creating a MutableSharedFlow.

The onBufferOverflow parameter in SharedFlow provides a strategy to handle situations where the buffer is full and a new value is emitted. It allows you to specify the behavior of the flow when the buffer, including any extra buffer capacity and replay buffer, is full. For example, with the usage of BufferOverflow.DROP_OLDEST, the oldest value in the buffer will be dropped to make space for the newest value.

This is how we use extraBufferCapacity and onBufferflow in SharedFlow:

And this is the result of it:

SharedFlow with repeatOnLifecycle API and Buffer

Although the buffer sounds like a solution for stacking values when we are in the background, it hasn’t made any difference at all.

On the other hand, replaying allows new collectors to receive some of the most recently emitted values immediately upon collecting from the SharedFlow. This is controlled by the replay parameter, which specifies the number of latest values that should be replayed to new collectors.

This is how we use replay in SharedFlow:

And this is the result of it:

SharedFlow with repeatOnLifecycle API and Replay

You probably guessed it right. Our flow is just repeating the last 3 values because of the replay = 3 setting. So we need another plan, but don't forget to play around with SharedFlow in the Bubbles/sharedflow branch.

Open the Channels!

And here we are, looking at Channels, our hero. Channels provide a way for two coroutines to communicate with each other and transfer information. They offer a convenient way to structure concurrent code, allowing data to be sent between coroutines in a sequential, buffered, or fan-out manner, thus facilitating more organized and maintainable asynchronous programming.

Channels synchronize data transfer, suspending the sender until a receiver is available, and vice versa. This ensures that elements are passed only when both sides are ready. But with the help of Buffered Channels, we can allow senders to send multiple elements before suspending.

Enough talk. Let’s get to work:

Forward to line

In this code snippet, we are creating a private Channel. The Channel.BUFFERED setting allows us to make our channel buffered. You can write an integer number here. For Channel.BUFFERED the default capacity is 64.

And for collecting this Channel in our Activity, we need to take it as a flow. So we are doing it in counterFlow with recieveAsFlow().

Forward to line

After collecting our Channel as flow this what we are achieving:

Channel (Buffered) with repeatOnLifecycle API

🎉 WE DID IT! Even when we are in the background, Channels buffer the values until we come back to the app. Don’t forget to check out the Bubbles/channel branch for final result.

Keep it up!

Thank you for working together with me on that walkthrough. As I said in the beginning, this message bubble example really helped me to understand many key concepts of Kotlin Flows, Channels, Shared/State Flows, and the repeatOnLifecycle API. But reactive programming in Kotlin is a huge topic. For this walkthrough Channels seemed like best solution. But for other cases you can find another tool more helpful. There are a lot of cases for many different scenarios, so don’t stop learning and think more!

Feel free to connect with me and reach out if you have any further inquiries or would like to discuss anything related to the topics we’ve covered.

https://www.linkedin.com/in/onuremrekaradag/

Bye!

--

--