LiveData related articles
LiveData is one of the most popular Android Jetpack components. Nowadays, many applications use it. Unfortunately, this solution has some limitations, and we will discuss them in this article.
I propose to replace LiveData with StateFlow and SharedFlow from Kotlin Flow because it solves LiveData issues and gives few additional benefits.
What is Kotlin Flow?
Flow is an asynchronous data stream that sequentially emits values and completes normally or with an exception.
Read more in the documentation
If you are not familiar with Kotlin Flow, you can read more in the official documentation.
Problems with LiveData
LiveData is used by many Android applications, but this structure has some downsides:
- LiveData is a structure that is readable and writable only on the main thread
LiveData is a structure that is readable and writable only on the main thread
Even if we can post value from the main thread, LiveData's value will be updated from the main thread internally.
This means that we will often change threads (worker thread → main thread) when we want to update the LiveData object.
The same situation will happen with the reading value of the LiveData object. If we check the observe
method’s internal implementation, we can see that this method can be used only from the main thread.
You can read more about the internal work of LiveData here.
Advantages of StateFlow
A state flow is a hot flow because its active instance exists independently of the presence of collectors. Its current value can be retrieved via the value property. State flow never completes. A call to Flow.collect on a state flow never completes normally, and neither does a coroutine started by the Flow.launchIn function. An active collector of a state flow is called a subscriber.
Read more about StateFlow here.
StateFlow solves issues that LiveData has.
Replacing LiveData with Flow
We have the two most popular scenarios of using LiveData object in ViewModel classes:
- observing emitted value or latest availble value (LiveData)
- observing value only once (SingleLiveEvent)
The SingleLiveData
is used when we want to receive value only once; it is usually used when we want to send a warning message or an action (navigate to a specific screen).
Let's image that we are working on the "Task Manager" application, and we are handling the following cases in the TasksViewModel
class:
- displaying all tasks
- emitting an action when we want to navigate to the task details screen
class TasksViewModel(
private val taskRepository: TaskRepository
) : ViewModel() {
private val _uiState = MutableLiveData<UiState<List<Task>>>()
val uiState: LiveData<UiState<List<Task>>>
get() = _uiState
val action = SingleLiveEvent<Action>()
fun loadData() {
_uiState.value = UiState.Loading
viewModelScope.launch {
taskRepository.getAllTasks().collect {
_uiState.value = UiState.Success(it)
}
}
}
...
}
The main difference between these two cases is that when we observe the uiState
object’s values, we want to receive changes in this object and the last emitted value after screen rotation. In the case of observing the action
object, we want to be notified only when the object is emitted. Let’s look at how we can replace LiveData objects in both of these cases with StateFlow and SharedFlow.
Collect the latest emitted data
We can replace LiveData<T>
with StateFlow<T>
when we want to be notified when the object is changed and receive the last emitted value after screen rotation and other cases when Activity/Fragment will be resumed. The state Flow requires an initial value.
class TasksViewModel(
private val taskRepository: TaskRepository
) : ViewModel() {
private val _uiState = MutableStateFlow<UiState<List<Task>>>(UiState.Loading)
val uiState: StateFlow<UiState<List<Task>>>
get() = _uiState
fun loadData() {
_uiState.value = UiState.Loading
viewModelScope.launch {
taskRepository.getAllTasks().collect {
_uiState.value = UiState.Success(it)
}
}
}
...
}
So, in this case, UiState.Loading
is an initial value for the tasksLiveData
object. When the correct value is emitted from ViewModel, and after changing screen rotation, this value will be emitted again as the last available value.
Let's collect the value in the Fragment:
class TasksFragment : Fragment(R.layout.fragment_tasks) {
...
private fun observeData() {
viewLifecycleOwner.lifecycleScope.launch {
tasksViewModel.uiState.collect {
handleUiState(it)
}
}
...
}
...
}
Collect emitted data once
In the situation when we want to collect data only once, the SharedFlow
is a good choice.
SharedFlow is hot Flow that shares emitted values among all its collectors in a broadcast fashion, so that all collectors get all emitted values. A shared flow is called hot because its active instance exists independently of the presence of collectors.
Read more about SharedFlow here.
We can configure SharedFlow
with a replay
parameter, which can determine how many times the values can be collected.
Let's change the type of the action
from SingleLiveEvent<T>
to SharedFlow<T>
.
class TasksViewModel(
private val taskRepository: TaskRepository
) : ViewModel() {
private val _action = MutableSharedFlow<Action>(replay = 0)
val action: SharedFlow<Action>
get() = _action
fun openDetails() {
...
viewModelScope.launch {
_action.emit(Action.NAVIGATE_TO_DETAILS)
}
}
...
}
The final step is to update the Fragment:
class TasksFragment : Fragment(R.layout.fragment_tasks) {
...
private fun observeData() {
viewLifecycleOwner.lifecycleScope.launch {
tasksViewModel.action.collect {
handleAction(it)
}
}
...
}
...
}
When screen rotation happens, the value won't be collected one more time because of a combination of the SharedFlow
type and replay = 0
parameter.
Summary
LiveData<T>
is used in many applications because it's a part of the Jetpack family and this component is easy to use for updating the UI state of the application. However, this component has a few issues:
- LiveData is readable and writable only on the main thread
However, if you already use Kotlin Flow, you can easily replace LiveData
with StateFlow
and SharedFlow
and have additional benefits:
- Both StateFlow and SharedFlow are readable and writable in different coroutine contexts
- StateFlow supports an initial value
- Both StateFlow and SharedFlow support all Flow operations, like a map, zip, filter, etc.