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 |
|
|
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() } } }
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")) } }
// 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) }
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) } } }
// 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) } } } }
// 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:
Post a Comment