Kotlin flows in Jetpack Compose: the ultimate guide

9 min read
June 21, 2023

If you have any experience in Android development, chances are you had to fetch data from a remote API, display that data on UI, and provide real-time updates that users can see. 

With the introduction of Jetpack compose, the whole paradigm of creating UI in Android is shifted. 

We went from imperative UI with XMLs to a declarative way of creating UI, where we describe each component. The byproduct of the declarative way of composing UI is, of course state. 

In this blog post we’re gonna dive into Kotlin flows, what they are, and how they interact with new ways of creating UI in Android.

What are Kotlin flows in Android development  

First of all, what are flows in Android development?

When talking about flows we can think of them as streams of data whose values are being computed asynchronously.

 It’s important to note that there are 2 types of flows: hot flows and cold flows. 

By definition, cold flows don’t produce any values until a terminal operator is used on them. 

Simply put, flows won’t return any data until we collect them with one of the operators such as collect(), first(), toList() to name a few.

You can see the full list of terminal operators here

Hot flows on the other hand will start producing values as soon as they are created, regardless if nobody is consuming them. 

Some common use cases for hot flows could be: 

  • Real-time updates – We could use hot flows to emit updates from server or database, and multiple subscribers could use them to update UI.
  • Sensor data – In this use case we can create a hot flow that emits sensor data from an accelerometer or the GPS and calculate the remaining time of our destination. 

The great thing about flows is that they’re designed to work seamlessly with coroutines.

This gives us the ability to fetch data from different data sources or update the UI without blocking the main thread in Android.

When talking about flows we must also mention components that are involved in transferring data between different parts of the application.

If we imagine flows as pipelines, the components that are required to make the pipeline work are: 

  • Producer – Puts data inside the pipeline
  • Consumer – Receives data from the pipeline 

Also, we have a 3rd component that is not essential for the pipeline to work and that is the Intermediary. Intermediaries can modify each value in the flow before they are put back into the pipeline and ready to be consumed by consumers. 

3 state flow components.

Now let’s talk about the State Flow.

What is a StateFlow

With the introduction of Jetpack Compose, we have a new type of flow called StateFlow. 

StateFlow itself extends Flow and adds “state” semantics to it. Unlike regular Flows, StateFlow provides built-in lifecycle awareness. That means collecting StateFlow within a composable will start only after the component is active.

Let’s see how to convert regular flow into StateFlow. 

For the sake of simplicity, let’s say we want to show a list of active users and update the UI every time someone new goes offline or becomes active. 

class FeedViewModel : ViewModel() {
   val activeUsers = flow<List<String>> { emit(listOf("user1", "user2", "user")) }
}

To convert a regular flow to a StateFlow we can simply call the .stateIn() method on the Flow. 

stateIn method accepts three parameters: CoroutineScope, Sharing method, and default value. 

class FeedViewModel : ViewModel() {
   val activeUsers = flow<List<String>> { emit(listOf("user1", "user2", "user")) }
       .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}

Let’s break down stateIn function parameters and see how this function works. 

The first parameter that we need to pass is the coroutine scope in which our flow will be executed.

 There are a few different coroutine scopes that we can use depending on the context in which we’re using the flow. In this case, we’re inside the ViewModel class so in this example, we’ll be using viewmodelScope.

SharingStarted is an interface that is used to define the behavior of shared coroutines. Depending on the problem we’re trying to solve we have few options to chose from:

  • SharingStarted.WhileSubscribed – Coroutine will be active as long as it has at least one active collector. In the case of multiple collectors, once the last one is canceled or completed, the coroutine will be paused or canceled.
  • SharingStarted.Eagerly – Coroutine will start regardless of whether there are active collectors or not. 
  • SharingStarted.Lazily – Coroutine will start only when there is at least one active collector. 

We’ll talk more about SharingStarted commands in the Collecting Flows section. The third parameter is the default value that will be emitted in case the coroutine does not produce value. 

 

How to collect flows

We mentioned already that in order to consume flow we have to use one of the terminal operators such as collect. 

When it comes to Jetpack Compose and consuming flows, things are a little bit different. Because now we have to deal with the state, Google provided us with the collectAsState() function. 

collectAsState() is used to collect values from a Flow and convert them into state objects. 

So every time a new value is posted into a pipeline returned state object will be updated which then will cause recomposition of every composable function that is using our flow value. 

Now, let’s go back to our example of updating the UI whenever the user’s information about his activity status changes. 

class FeedViewModel : ViewModel() {
   val activeUsers = flow<List<String>> { emit(listOf("user1", "user2", "user")) }
       .stateIn(viewModelScope, SharingStarted.Lazily, emptyList())
}

Inside of our activity or fragment, we’ll subscribe to that flow and update UI accordingly. 

@Composable
fun UserFeedScreen(userViewModel: UserViewModel) {
   val searchText by userViewModel.searchTextState.collectAsStateWithLifecycle()
   val clients by userViewModel.clients.collectAsState()
   ClientRoute(
       clients,
       searchText = searchText,
       searchWidgetState = clientViewModel.searchWidgetState.value,
       onTextChange = { clientViewModel.updateSearchTextState(newValue = it) },
       onSearchTrigger = { clientViewModel.updateSearchWidgetState(newValue = SearchWidgetState.OPENED) },
       onCloseClick = { clientViewModel.updateSearchWidgetState(newValue = SearchWidgetState.CLOSED) },)
}

However there are a few problems with this approach, let’s see what they are and how to fix them.

Let’s say you have a simple counter that counts indefinitely. 

class MyViewModel : ViewModel() {
   private var secondsCounter = 0
   val secondsPassed = flow {
       while (true) {
           delay(1000)
           secondsCounter += 1
           emit(secondsCounter)
       }
   }.stateIn(viewModelScope,SharingStarted.Lazily,0)
}

For the sake of simplicity let’s say we have just a plain Textbox where we display the values of that counter.

@Composable
fun MyScreen(myViewModel: MyViewModel) {
   Surface {
       val secondsPassed by myViewModel.secondsPassed.collectAsState(0)
       Text(text = secondsPassed.toString())
   }
}

One of the problems with this approach lies in SharingStarted.Lazily that we used on our flow. 

If you remember from before SharingStarted.Lazily will launch the coroutine as soon as the first collector subscribes to the flow, but the coroutine will be canceled only when the scope that is tied to is canceled, in our case viewmodel scope. 

This leads to the problem where the coroutine scope will be active even if we leave our screen and go to the next one, since we didn’t pop the previous screen from the backstack, the viewmodel scope is still active thus making our flow run even if we don’t need it anymore. 

stateIn(viewModelScope,SharingStarted.WhileSubscribed(5000L),0)

We can fix that by using SharingStarted.WhileSubscribed().

Since collectAsState() function is a composable function that knows the state of our composition, it can tell when our composable disappeared and is no longer active, so the coroutine will be canceled. 

Unfortunately, this is not a good solution either. 

Imagine if the user rotates the screen, the whole screen will be destroyed and recreated along with our ViewModel, making our counter start from 0. 

Google’s recommended way to avoid this is by passing parameter to our whileSubscribed() function. This parameter tells our coroutine how long it has to wait after the last collector is unsubscribed to cancel itself.

If in the meantime someone is subscribed to our flow before time runs out, coroutine will continue as if nothing happened.

The recommended waiting time was set to be 5000 milliseconds.

But does this solution solve everything?

Well no, while this fixes some of our problems, it can be inefficient to collect flows in this way. 

Because collectAsState() is not lifecycle-aware, when putting our application in the background, the coroutine will still run and flow will emit values.

Rarely do we need to listen for UI changes in the background. In such cases, it might be better to choose foreground services.  

Here comes collectAsStateWithLifecycle() to the rescue. 

To use collectAsStateWithLifecycle() function you will need to import this line of code: 

implementation "androidx.lifecycle:lifecycle-runtime-compose:$lifecycle_version"

This will make sure that our composables are only recomposed when the composing component is active and visible on the screen. 

It will automatically stop collecting values when the component is not active anymore, preventing unnecessary updates and reducing resource usage. 

@Composable
fun MyScreen(myViewModel: MyViewModel) {
   Surface {
       val secondsPassed by myViewModel.secondsPassed.collectAsStateWithLifecycle()
       Text(text = secondsPassed.toString())
   }
}

Problem solved!

Flows in Jetpack Compose: summary

Jetpack compose gave us a new way of creating UI along with that new set of challenges on how to update UI and display in real-time changes to the user. 

There are a number of ways of achieving that goal but using state flow seems to be the best solution. 

Flows with their ability to utilize coroutines offer us an asynchronous way of fetching and preparing data so we don’t have to block the main thread and leave our app responsive for users and react to the changes automatically.

CollectAsStateWithLifecycle is a new API developed by Google that lets you collect your flows without worrying about the lifecycle and cancellation of your coroutine scopes.

If you’d like to read more about all things Android, feel free to check out our blog.

Categories
Written by

Domagoj Rundek

Android Developer

Related articles