Sunday, 3 July 2022

LiveData & Flow

As part of developing apps on android, it is quite important things is how effectively and efficiently the UI can be updated on demand to the changes in data at lower layers, decoupling those layers, handling callbacks. Separation of these layers really helps on the testing part. (i.e. constructing the domain and data layer independently).   Since the legacy android code i.e. code which is written on imperative styles we would have writing lot of boilerplate code to handle the data changes using AsyncTask, callbacks, handlers etc.  But now the development has notably evolved and I would say much easier if one understands the basics.  There are always multiple ways to perform a given task within android and one can only decide the best alternative if the basic difference, pros, and cons of all options are mastered.


Just Glimpses:


LiveData: LiveData is a lifecycle aware observable data holder (means it knows the lifecycle of the activity or a fragment) use it when you play with UI elements (views).


Flow: Flow (cold stream) – In general think of it like a stream of data flowing in a pipe with both ends having a producer and consumer running on a coroutines.


StateFlow: (hot stream) does similar things like LiveData but it is made using flow by kotlin guys and only difference compare to LiveData is it’s not lifecycle aware but this is also been solved using repeatOnLifecycle api’s, so whatever LiveData can do StateFlow can do much better with power of flow’s api. StateFlow won’t emit same value.


SharedFlow: (hot stream) – name itself says it is shared, this flow can be shared by multiple consumers, I mean if multiple collect calls happening on the sharedflow there will be a single flow which will get shared across all the consumers, unlike normal flow.


What are the above highlighted terms, let’s try to address here what are those in details and that helps us to choose among LiveData, Flow, SharedFlow, and StateFlow


LiveData:  Live data is part of Android Architecture Components.  


It is an Observable data class:  it can hold data and that data can be observed from android components. Meaning it can be observed by other components — most profoundly UI controllers (Activities/Fragments).  


It is Lifecycle aware—it sends updates to our components i.e. UI (Activities/Fragments) only when our view is in the active state.

 

Pros

Cons

  • Would always get the latest data
  • Always check if the subscriber is active before publishing data.
  • No memory leaks
  • No crashes or unnecessary ANR
  • Configuration changes update
  • Lack of control over the execution context
  • Threading issue especially when used in Repositories
  • Not built on top of Coroutines and Kotlin
  • Lack of seamless data integration across between database and UI especially using Room.
  • Lots of Boiler Plate Codes especially while using Transformations

 


Flow:  Built on top of a coroutines, a flow emits multiple values sequentially.  It’s a stream of data that can be computed sequentially.  Flow can handle streams of values, and transform data in complex multi-threaded ways using an intermediate operators to modify the stream without consuming values. (So basically it’s an alternate to RxJava.)

 

Flows by nature are not lifecycle aware unlike LiveData. Which makes sense as it’s not a part of android component but a type from Kotlin language. However, this can be resolved by responsibly collecting flow values within lifeCycleScopes via coroutines.

 

Flow is declarative/cold: It can only be executed on collection and there are hot flows as well (SharedFlow and StateFlow).

 

COLD: Stops emission when any collector is not active. HOT: It remains in memory as long as the flow is collected or as long as any other references to it exist from a garbage collection root.

 

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. Hence state flow will have always a value.
  • State flow is Hot Flow because it starts emitting values even though there is no consumer and those data will be remains in the memory
  • State flow always has an initial value, replays one most recent value to new subscribers, does not buffer any more values, but keeps the last emitted one.
  • All methods of state flow are thread-safe and can be safely invoked from concurrent coroutines without external synchronization.
  • When you collect state flow through collect{} and if the consumer gets recreated due to configuration change it will re-call collect{}
  • You can convert Cold flow to state flow using stateIn () operator.

SharedFlow:

  • A SharedFlow is a highly-configurable generalization of StateFlow, that emits all value to all consumers in a broadcast fashion.
  • 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.
  • A default implementation of a shared flow that is created with MutableSharedFlow() constructor function without parameters has no replay cache nor additional buffer.
  • All methods of shared flow are thread-safe and can be safely invoked from concurrent coroutines without external synchronization.
  • When you collect shared flow through collect{} and if the consumer gets recreated due to configuration change it will not re-call collect{}
  • You can convert Cold flow to shared flow using shareIn () operator.

Extras….


StateFlow and LiveData have similarities: Both are observable data holder classes, and both follow a similar pattern when used in your app architecture.

The StateFlow and LiveData do behave differently: StateFlow requires an initial state to be passed into the constructor, while LiveData does not.


LiveData.observe() automatically unregisters the consumer when the view goes to the STOPPED state, whereas collecting from a StateFlow or any other flow does not stop collecting automatically. To achieve the same behavior,you need to collect the flow from a Lifecycle.repeatOnLifecycle block.


StateFlow Vs. SharedFlow: The main difference between a SharedFlow and a StateFlow is that a StateFlow takes a default value through the constructor and emits it immediately when someone starts collecting, while a SharedFlow takes no value and emits nothing by default.


Here we see the code snippet in the blog using LiveData, SharedFlow and StateFlow used in ViewModel and that how it handles with the retrofit response and updates the same to the Fragment.


Full Source Code Reference in Branch



updateView(..) in the Fragment (Code Snippet to update the UI View handle)


private fun updateView(res: Resource<Summary>) {
        when (res.status) {
            Status.LOADING -> {
                binding.progressBar.visibility = View.VISIBLE
            }
            Status.SUCCESS -> {
                binding.progressBar.visibility = View.GONE
                res.data?.let {
                    updateCards(it.global)
                    it.countries?.let {
                        statusAdapter.setData(it)
                    } ?: kotlin.run {
                        Toast.makeText(activity, "No Data Available " + res.msg, Toast.LENGTH_LONG)
                            .show()
                    }
                }
            }
            Status.ERROR -> {
                binding.progressBar.visibility = View.GONE
                Toast.makeText(
                    activity,
                    "Something went wrong... Please contact admin " + res.msg,
                    Toast.LENGTH_LONG
                ).show()
            }
        }
    }

LiveData: In ViewModel class:

private val _summaryLiveData = MutableLiveData(Resource.loading(Summary()))
val summaryLiveData = _summaryLiveData as LiveData<Resource<Summary>>

// LiveData Object gets updated

fun getCovidStatusLiveData() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                _summaryLiveData.postValue(Resource.loading(null))
                val summaryResponse =
                    APIClient.createService(tClass = CovidStatusAPI::class.java)
                        .getCountriesSummary()
                summaryResponse.takeIf { it.isSuccessful }?.let {
                    _summaryLiveData.postValue(Resource.success(it.body() as Summary))
                } ?: kotlin.run {
                    _summaryLiveData.postValue(Resource.error("Error", null))
                }
            } catch (e: Exception) {
                _summaryLiveData.postValue(Resource.error(e.message ?: "Err", null))
            }
        }
    }

// Using LiveData Scope (It uses emits)
fun getCovidStatusLiveDataScope() = liveData {
        try {
            emit(Resource.loading(null))
            val summaryResponse =
                APIClient.createService(tClass = CovidStatusAPI::class.java).getCountriesSummary()
            summaryResponse.takeIf { it.isSuccessful }?.let {
                emit(Resource.success(it.body() as Summary))
            } ?: kotlin.run {
                emit(Resource.error("Error", null))
            }
        } catch (e: Exception) {
            emit(Resource.error(e.message ?: "Err"))
        }
    }

In Fragment (LiveData observer):

// Observing the LiveData object of viewmodel 
viewModel.summaryLiveData.observe(viewLifecycleOwner) {
            updateView(it)
        }

// Observing the direct livedata scope function of viewmodel 
viewModel.getCovidStatusLiveDataScope().observe(viewLifecycleOwner){
            updateView(it)
        }

StateFlow & SharedFlow: In ViewModel class:

private val _summaryStateFlow = MutableStateFlow(Resource.loading(Summary()))
val summaryStateFlow = _summaryStateFlow as StateFlow<Resource<Summary>>

private val _summarySharedFlow = MutableSharedFlow<Resource<Summary>>()
val summarySharedFlow = _summarySharedFlow as SharedFlow<Resource<Summary>>

// Using SharedFlow
fun getCovidStatusSharedFlow() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                _summarySharedFlow.emit(Resource.loading(null))
                val summaryResponse =
                        APIClient.createService(tClass = CovidStatusAPI::class.java)
                                .getCountriesSummary()
                summaryResponse.takeIf { it.isSuccessful }?.let {
                    _summarySharedFlow.emit(Resource.success(it.body() as Summary))
                } ?: kotlin.run {
                    _summarySharedFlow.emit(Resource.error("Error", null))
                }
            } catch (e: Exception) {
                _summarySharedFlow.emit(Resource.error(e.message ?: "Err", null))
            }
        }
    }

// Using StateFlow
fun getCovidStatusStateFlow() {
        viewModelScope.launch(Dispatchers.IO) {
            try {
                _summaryStateFlow.emit(Resource.loading(null))
                val summaryResponse =
                        APIClient.createService(tClass = CovidStatusAPI::class.java)
                                .getCountriesSummary()
                summaryResponse.takeIf { it.isSuccessful }?.let {
                    _summaryStateFlow.emit(Resource.success(it.body() as Summary))
                } ?: kotlin.run {
                    _summaryStateFlow.emit(Resource.error("Error", null))
                }
            } catch (e: Exception) {
                _summaryStateFlow.value = Resource.error(e.message ?: "Err", null)
            }
        }
    }

In Fragment (Handling state and shared flow emitted values): Since we know Flows by nature are not lifecycle aware, so it need to be handled like below.

// StateFlow to make it as lifecycle aware.
viewLifecycleOwner.lifecycleScope.launch {
             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
                     viewModel.summaryStateFlow
                         .collectLatest {
                             updateView(it)
                         }
                 }
             }
         }

// SharedFlow to make it as lifecycle aware.
viewLifecycleOwner.lifecycleScope.launch {
             viewLifecycleOwner.repeatOnLifecycle(Lifecycle.State.STARTED) {
                 launch {
                     viewModel.summarySharedFlow.collectLatest {
                         updateView(it)
                     }
                 }
             }
         }

But doing above for all stateflow or sharedflow object is tedious so we can write one common inline function in Utils to make it as easy for the lifetime.

// This Take care of LifeCycle Aware
inline fun <T> Flow<T>.launchAndCollectIn(
    owner: LifecycleOwner,
    minState: Lifecycle.State = Lifecycle.State.STARTED,
    crossinline block: suspend CoroutineScope.(T) -> Unit
) {
    owner.lifecycleScope.launch {
        owner.repeatOnLifecycle(minState) {
            collect {
                block(it)
            }
        }
    }
}

viewModel.summarySharedFlow.launchAndCollectIn(viewLifecycleOwner) {
            updateView(it)
        }

viewModel.summaryStateFlow.launchAndCollectIn(viewLifecycleOwner) {
            updateView(it)
        }

No comments: