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:
class ArticlesViewModel(
private val articlesRepository: ArticlesRepository, // fetches from network or db, etc
private val uiMapper: ArticlesUiMapper
) : ViewModel() {
private val _uiState = MutableLiveData<ArticlesUiState>(ArticlesUiState.Loading)
val uiSate: LiveData<ArticlesUiState> = _uiState
init {
loadArticles()
}
private fun loadArticles() {
viewModelScope.launch {
_uiState.value = ArticlesUiState.Loading
runCatching {
articlesRepository.getArticles()
}.onSuccess { articles ->
val uiModels = uiMapper.map(articles)
_uiState.value = ArticlesUiState.Content(uiModels)
}.onFailure { error ->
val errorMessage = uiMapper.mapErrorMessage(error)
_uiState.value = ArticlesUiState.Error(errorMessage)
}
}
}
}
- a fragment:
class ArticlesFragment : Fragment() {
private val viewModel: ArticlesViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.uiState.observe(viewLifecycleOwner) { uiState ->
// handle ui state
}
}
}
- 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
:
class ArticlesViewModel(
private val articlesRepository: ArticlesRepository, // fetches from network or db, etc
private val uiMapper: ArticlesUiMapper
) : ViewModel() {
private val _uiState = MutableStateFlow<ArticlesUiState>(ArticlesUiState.Loading)
val uiSate: StateFlow<ArticlesUiState> = _uiState
init {
loadArticles()
}
private fun loadArticles() {
viewModelScope.launch {
_uiState.value = ArticlesUiState.Loading
runCatching {
articlesRepository.getArticles()
}.onSuccess { articles ->
val uiModels = uiMapper.map(articles)
_uiState.value = ArticlesUiState.Content(uiModels)
}.onFailure { error ->
val errorMessage = uiMapper.mapErrorMessage(error)
_uiState.value = ArticlesUiState.Error(errorMessage)
}
}
}
}
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.
/**
* [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
*
* This scope will be cancelled when the [Lifecycle] is destroyed.
*
* This scope is bound to
* [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
*/
val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
get() = lifecycle.coroutineScope
To collect a stateflow values, we can use the fragment’s viewLifecycleOwner.lifecycleScope
:
class ArticlesFragment : Fragment() {
private val viewModel: ArticlesViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.uiState.onEach { uiState ->
// handle ui state
}.launchIn(viewLifecycleOwner.lifecycleScope)
}
}
You can add an extension to your fragment to directly get the viewLifecyclScope
val Fragment.viewLifecycleScope: LifecycleCoroutineScope
get() = viewLifecycleOwner.lifecycleScope
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:
fun <T> Flow<T>.launchInStarted(scope: LifecycleCoroutineScope): Job =
scope.launchWhenStarted { collect() }
fun <T> Flow<T>.launchInResumed(scope: LifecycleCoroutineScope): Job =
scope.launchWhenResumed { collect() }
fun <T> Flow<T>.launchInCreated(scope: LifecycleCoroutineScope): Job =
scope.launchWhenCreated { collect() }
(a feature request was opened on Google Issue Tracker to provide those extensions)
Now, to collect uiState
values, we can use launchInStarted:
class ArticlesFragment : Fragment() {
private val viewModel: ArticlesViewModel
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
viewModel.uiState.onEach { uiState ->
// handle ui state
}.launchInStarted(viewLifecycleScope)
}
}
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)