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

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)
            }
        }
    }
}
class ArticlesFragment : Fragment() {
  private val viewModel: ArticlesViewModel

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    viewModel.uiState.observe(viewLifecycleOwner) { uiState ->
      // handle ui state
    }
  }
}

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:

Cons:

StateFlow

Pros:

Cons:

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

Resources