Detailed guide about Kotlin Flow

Detailed guide about Kotlin Flow

What is Kotlin Flow, when and how to use it? Lets learn about it.

Flows are built on the Kotlin coroutine which can emit multiple values sequentially over time. If we think about flow, it is just a stream of data which can be produced and consumed. A flow is very similar to an Iterator that produces a sequence of values, but it uses suspend functions to produce and consume values asynchronously.

So, we can tell, a flow is a stream of multiple, asynchronously computed values. Flows emit values as soon as they are done computing them. A flow consists of a producer and a consumer.

So, there are mainly two components a flow has,

  1. Producer: A producer produces data that is added to the stream. Thanks to coroutines, flows can also produce data asynchronously.

  2. Consumer: A consumer consumes data from the emitted streams.

Types of flow:

Mainly flows are two types, SharedFlow and StateFlow. But to understand these we have to understand cold flow and hot flow first.

Cold Flow: A cold stream does not start producing values until one starts to collect them.

Hot Flow: A hot stream on the other hand starts producing values immediately.

There are two types of hot flow:

  1. StateFlow: State Flow is similar to normal flow but it holds the state. When you initialize the state flow you always need to tell the initial value of state flow. Also in StateFlow you will always get the most recently emitted value. You cannot get previously emitted values. Thus we have to give an initial value in Stateflow so that it can give us immediate data though it doesn't produce any data itself. You also can convert Cold flow to state flow using shareIn*() operator.*

  2. SharedFlow: A shared flow keeps a specific number of the most recent values in its replay cache. Every new subscriber first gets the values from the replay cache and then gets new emitted values. The maximum size of the replay cache is specified when the shared flow is created by the replay parameter. You can easily configure sharedflow to give you previously emitted data besides the most recent data. It is also convertible to cold flow using shareIn() operator.

How to convert hot flow into cold flow?

StateFlow is a hot flow—it remains in memory as long as the flow is collected or while any other references to it exist from a garbage collection root. You can turn cold flows hot by using the shareIn operator.

Using the callbackFlow created in Kotlin flows as an example, instead of having each collector create a new flow, you can share the data retrieved from Firestore between collectors by using shareIn. You need to pass the following:

  • A CoroutineScope is used to share the flow. This scope should live longer than any consumer to keep the shared flow alive as long as needed.

  • The number of items to replay to each new collector.

  • The start behavior policy.

class NewsRemoteDataSource(...,
    private val externalScope: CoroutineScope,
) {
    val latestNews: Flow<List<ArticleHeadline>> = flow {
        ...
    }.shareIn(
        externalScope,
        replay = 1,
        started = SharingStarted.WhileSubscribed()
    )
}

In this example, the latestNews flow replays the last emitted item to a new collector and remains active as long as externalScope is alive and there are active collectors. The SharingStarted.WhileSubscribed() start policy keeps the upstream producer active while there are active subscribers. Other start policies are available, such asSharingStarted.Eagerly to start the producer immediately or SharingStarted.Lazily to start sharing after the first subscriber appears and keep the flow active forever.

Creating flow and Consuming the produced value:

  1. You can create flow by using flow builder. You’ll start by creating a simple Flow. To create a Flow, you need to use a flow builder. You’ll start by using the most basic builder – flow { ... }. Add the following code above main():

       val namesFlow = flow {
         val names = listOf("Jody", "Steve", "Lance", "Joe")
         for (name in names) {
           delay(100)
           emit(name)
         }
       }
    
  2. Using flowOf: There are other Flow builders that you can use for an easy Flow declaration. For example, you can use flowOf() to create a Flow from a fixed set of values:

      val namesFlow = flowOf("Jody", "Steve", "Lance", "Joe")
    
  3. Or you can convert various collections and sequences to a Flow by using asFlow():

     val namesFlow = listOf("Jody", "Steve", "Lance", "Joe").asFlow()
    

Using flow with database:

In ForecastDao.kt and add a call to getForecasts(). This method returns Flow<List<DbForecast>>:

@Query("SELECT * FROM forecasts_table")
fun getForecasts(): Flow<List<DbForecast>>

Next, in WeatherRepository.kt and add a function called getForecasts:

fun getForecasts(): Flow<List<Forecast>>

Next, add the implementation to WeatherRepositoryImpl.kt:

override fun getForecasts() =
    forecastDao
      .getForecasts()
      .map { dbMapper.mapDbForecastsToDomain(it) }

Now in ViewModel lets get the data,

val forecasts: LiveData<List<ForecastViewState>> = weatherRepository
    //2
    .getForecasts()
    //3
    .map {
      homeViewStateMapper.mapForecastsToViewState(it)
    }
    //4
    .asLiveData()

First, you declare forecasts of the LiveData<List<ForecastViewState>> type. The Activity will observe changes in forecasts. forecasts could have been of the Flow<List<ForecastViewState>> type, but LiveData is preferred when implementing communication between View and ViewModel. This is because LiveData has internal lifecycle handling!

Collecting Flow

There are several ways you can collect from a flow, they are through ViewModel.viewModelScope.launch, LifeCycleCoroutineScope.LaunchWhenStarted and LifeCycle.RepeatOnLifecycle.

  1. Collect Flow Using ViewModel.viewModelScope.launch{}

fun viewModelScopeCollectFlow() {   
        viewModelScope.launch {  
            flow.collect {  value ->
                _state.value = value  
            } 
        }
    }
  1. Collect Flow Using LifeCycleCoroutineScope.LaunchWhenStarted()

  2.     lifeCycleScope.launchWhenStarted {
                    flow.collect { value ->
                        _state.value = value
                    }
                }
    
  3. Collect Flow Using LifeCycle.RepeatOnLifecycle()

    lifeCycleScope.launch {
            lifeCycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
                flow.collect { value ->
                    _state.value = value
                }
            }
        }

Conclusion

Flow's are very useful in kotlin android development. Besides using live data by using flow you will have extreme control over the state management and data processing in your application. Especially if you want to work with Jetpack Compose Flow is the best option for you and it works great with jetpack compose as it is built with kotlin on coroutines.