Migrating From LiveData to StateFlow

Federico Torres
Dec 9, 2020 · 4 min read

I’ve worked a lot with LiveData and I think it is a great solution to save the UI state and observe it from the view layer without worrying about lifecycle issues, but…

What is wrong with LiveData?

  • It is used everywhere, for example in the repository layer
  • Not knowing the difference between setValue() and postValue()

I don’t want to dive into the details of this two problems, a lot has been written about both of them, but to summarize: the first one will lead to performance issues and the second one might cause crashes and bugs in your app.

If you didn’t know this caveats of LiveData, here are some good articles talking about that.

What is StateFlow?

StateFlow is a state-holder observable flow that emits the current and new state updates to its collectors. The current state value can also be read through its value property. To update state and send it to the flow, assign a new value to the value property of the MutableStateFlow class.

It looks very similar to LiveData, but let's check the actual declaration in code

public interface StateFlow<out T> : SharedFlow<T>

So it is an interface extending from SharedFlow, which in turn extends Flow from the coroutines library, so this new data holder class is somehow related to coroutines!

Current LiveData Code

  • PostsFragment: it shows a list of posts
  • PostsViewModel: our interaction between repository and fragment

PostsFragment.kt:

viewModel.observeState().observe(requireActivity(), Observer {
renderState(it)
})
private fun renderState(state: PostsState) {
//update view according to the new state
}

PostsViewModel.kt:

private val state = MutableLiveData<PostsState>()fun initialize() {
state.value = PostsState()
...
}
fun observeState(): LiveData<PostsState> {
return state
}
private fun fetchPosts() {
CoroutineScope(Dispatchers.IO)).launch {
val posts = repository.getPosts()
viewModelScope.launch { (1)
emitNewState(
getState().copy(
loading = false,
posts = posts
)
)
}
}
}
@MainThread
private fun emitNewState(newState: PostsState) {
state.value = newState
}
private fun getState() = state.value ?: PostsState()

(1) This is because emitNewState() must be called from the main thread, so what we are doing here is changing the coroutine scope to execute our code in the main thread

Migration to StateFlow

app.gradle

dependencies{
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.4.2'
}

PostsFragment.kt:

lifecycleScope.launchWhenResumed { (1)
viewModel.observeState().collect { (2)
renderState(it)
}
}

(1): Here we are launching a new coroutine and assuring that the given block is only executed when the fragment is at least in RESUMED state
(2): We start observing the StateFlow by collecting the flow in the main thread

PostsViewModel.kt:

private val state = MutableStateFlow(PostsState()) (1)fun initialize() {
(2)
...
}
fun observeState(): StateFlow<PostsState> { (3)
return state
}
private fun fetchPosts() {
CoroutineScope(Dispatchers.IO).launch {
val posts = repository.getPosts()

val newState = getState().copy(
loading = false,
posts = posts
)
emitNewState(newState) (4)
}
}
private fun emitNewState(newState: PostsState) {
state.value = newState (5)
}
private fun getState() = state.value (6)

So we can see some differences when using StateFlow:

(1) MutableStateFlow ‘s constructor requires passing an initial state
(2) Due to this initial state passed in the constructor, we don’t need to do it manually
(3) Here we just changed the return type, note that for several reasons we don’t want to expose a MutableStateFlow, that’s why we are returning a StateFlow, which can only be collected / observed
(4) One of the most important advantages: updating the value of a StateFlow is thread-safe, so we can update it from any coroutine scope, making our code simpler and cleaner and less error prone
(5) As mentioned, we don’t need to update the value in the main thread, the fragment is responsible of collecting the flow in the main thread
(6) Contrary to LiveData, StateFlow’s value is not nullable, so we don’t have to be checking for a possible null value

Final Conclusion

By using StateFlow I can see this benefits:

  • It is inside the coroutines library, so it can be used in kotlin multiplatform projects
  • No need of null checks when accessing StateFlow’s value field
  • If you are using Flow or coroutines in your project, it should be very easy to migrate

Some warnings

  • StateFlow is conflated, it means that only the most recently sent value will be collected by subscribers, and if the new value is equal to the last emitted value, it will not be emitted again. The equality is implemented by the Any.equals contract
  • You have to take care of when and in which thread you are collecting the flow. For example if you use fragment’s lifecycle scope with launch() instead of launchWhenResumed() you might be executing view updates when the view of your fragment is destroyed.
    If you want to use launch() , to avoid that problem you can use viewLifecycleScope

If you come up with any other benefit or disadvantage please mention it!

Thank you!

The Startup

Get smarter at building your thing. Join The Startup’s +737K followers.