The Kotlin LiveData Alternative: StateFlow

As was noted back in the chapter on threads, Google is starting to push StateFlow as an alternative to LiveData for use in Kotlin projects.

You can learn more about StateFlow and SharedFlow in the "Opting Into SharedFlow and StateFlow" chapter of Elements of Kotlin Coroutines!

Fundamentally, StateFlow works just like LiveData:

The DiceFlow sample module in the Sampler project is a clone of DiceLight, except that it uses StateFlow in places where DiceLight uses LiveData. Hence, the repository and the view-state are unaffected, since they do not use LiveData. What are affected are MainMotor and MainActivity.

Impacts on MainMotor

Just as we have MutableLiveData and LiveData, we have MutableStateFlow and StateFlow. MainMotor in DiceFlow is identical to its equivalent in DiceLight, just with _results and results based off of MutableStateFlow and StateFlow:

package com.commonsware.jetpack.diceware

import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

private const val DEFAULT_WORD_COUNT = 6

class MainMotor(application: Application) : AndroidViewModel(application) {
  private val _results = MutableStateFlow<MainViewState>(MainViewState.Loading)
  val results = _results.asStateFlow()

  init {
    generatePassphrase(DEFAULT_WORD_COUNT)
  }

  fun generatePassphrase() {
    generatePassphrase(
      (results.value as? MainViewState.Content)?.wordCount ?: DEFAULT_WORD_COUNT
    )
  }

  fun generatePassphrase(wordCount: Int) {
    _results.value = MainViewState.Loading

    viewModelScope.launch(Dispatchers.Main) {
      _results.value = try {
        val randomWords = PassphraseRepository.generate(
          getApplication(),
          wordCount
        )

        MainViewState.Content(randomWords.joinToString(" "), wordCount)
      } catch (t: Throwable) {
        MainViewState.Error(t)
      }
    }
  }
}

As with MutableLiveData, we use setValue() to update the value in the MutableStateFlow. setValue() is a suspend function, so it needs to be called inside of a coroutine, but we already had a coroutine for use in calling PassphraseRepository.generate(), so that did not introduce a code change.

MutableLiveData supports being instantiated without a starting value. MutableStateFlow does not, so we initialize it to the Loading view-state.

And, to get a StateFlow from the MutableStateFlow, we call asStateFlow().

Impacts on MainActivity

MainActivity still observes results and passes the view-states to a when() to route them to appropriate rendering logic. However, consuming a StateFlow is a bit different than consuming a LiveData:

  override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)

    val binding = ActivityMainBinding.inflate(layoutInflater)

    setContentView(binding.root)

    lifecycleScope.launch {
      lifecycle.repeatOnLifecycle(Lifecycle.State.STARTED) {
        motor.results.collect { viewState ->
          when (viewState) {
            MainViewState.Loading -> {
              binding.progress.visibility = View.VISIBLE
              binding.passphrase.text = ""
            }
            is MainViewState.Content -> {
              binding.progress.visibility = View.GONE
              binding.passphrase.text = viewState.passphrase
            }
            is MainViewState.Error -> {
              binding.progress.visibility = View.GONE
              binding.passphrase.text = viewState.throwable.localizedMessage
              Log.e(
                "Diceware",
                "Exception generating passphrase",
                viewState.throwable
              )
            }
          }
        }
      }
    }
  }

We call collect() on the StateFlow, passing in our lambda expression to be invoked for each view-state, including the initial Loading one.

That collect() gets wrapped in repeatOnLifecycle(), called on the Lifecycle object representing this fragment’s view lifecycle. We will only collect events while we are in the started or resumed states repeatOnLifecycle(Lifecycle.State.STARTED), so we will not try processing view-states while our UI is in the background. And that in turn is wrapped in launch() on the LifecycleScope associated with this fragment’s view lifecycle, so the entire coroutine can be canceled cleanly when the fragment’s views are destroyed.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.