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.
StateFlow
and SharedFlow
in the "Opting Into SharedFlow and StateFlow" chapter of Elements of Kotlin Coroutines!
Fundamentally, StateFlow
works just like LiveData
:
-
StateFlow
caches a last-seen value - When the value changes, in addition to caching it,
StateFlow
passes the value to all current observers - A new observer immediately gets the last-seen value, in addition to any changes while the observer is observing
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.