Android, from LiveData to StateFlow
The following article will detail how we can replace the presentation layer’s livedatas with stateFlows and some caveats we encountered along the way. On the 30th of October, kotlinx.coroutines 1.4.0 was released, promoting StateFlow to stable API.
StateFlow
A StateFlow is a Flow
that can hold a value, its state. When the value is updated, flow collectors will receive the update.
Like List and MutableList, StateFlow comes with a MutableStateFlow.
How we use LiveData
At Fabernovel, all our new Android application are written in Kotlin.
Our presentation layer is composed of a (Jetpack) ViewModel
which exposes its data in a LiveData to the view (a Fragment).
The business layer is using coroutines and flows to handle asynchronous resources (making network requests, reading/writing to a database, etc).
- a viewModel:
- a fragment:
- Notes:
viewModelScope
is provided by androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0 or higher.
Children jobs are cancelled when the viewmodel is cleared.
Pros and cons
Neither LiveData nor StateFlow is inherently superior to the other. They can both be used to safely pass data from a viewmodel to a view.
LiveData
Pros:
- Imperative APIs
Cons:
- Limited APIs
- Hard to combine multiple livedata
StateFlow
Pros:
- A lower level implementation of what LiveData represent, it is closely tied to Kotlin (through kotlinx.coroutines). While LiveData were made for Android, StateFlow were made for Kotlin and coroutines.
- Supported by Kotlin Multiplatform.
- Imperative APIs
- Can be used with Flow operators
Cons:
- StateFlow requires an initial state.
- Some caveats with Android lifecycle for data collection. (solutions are detailed down here)
Migrate to StateFlow
Let’s replace the livedata uiState
with a stateflow.
In ArticlesViewModel, since the APIs of StateFlow and LiveData are similar, we only need to replace the MutableLiveData
with a MutableStateFlow
and the LiveData
with a StateFlow
:
As for ArticlesFragment, we need to find a coroutine scope to collect the stateflow. Luckily androidx.lifecycle:lifecycle-lifecycle-ktx:2.2.0
adds extensions to LifecycleOwner
to use it as a coroutine scope.
To collect a stateflow values, we can use the fragment’s viewLifecycleOwner.lifecycleScope
:
You can add an extension to your fragment to directly get the viewLifecyclScope
However, there’s a difference between fun LiveData<T>.observe(LifecycleOwner, Observer<T>)
and Flow<T>.launchIn(CoroutineScope)
,
observe
will collect values only when the lifecycle state is STARTED or RESUMED, launchIn
, will collect values while the coroutine scope is not cancelled.
To fix this issue, we can create extensions to only collect values during certains lifecycle states.
androidx.lifecycle:lifecycle-lifecycle-ktx:2.2.0
already adds extensions to launch coroutines during certains states: launchWhenStarted
, launchWhenCreated
, etc.
Since launchIn(CoroutineScope)
is scope.launch { collect() }
, we can create launchInCreated
, launchInStarted
to collect values during thoses states:
(a feature request was opened on Google Issue Tracker to provide those extensions)
Now, to collect uiState
values, we can use launchInStarted:
Takeaways
- LiveData and StateFlow are both observable value holders. They have similar APIs.
- To collect a Flow using a
LifecycleOwner
, make sure the collected value is only collected during certain lifecycle state (usingLifecycleCoroutineScope.launchInStarted
extensions)