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)
}