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.

LiveData.postValue under the hood

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.

LiveData.postValue under the hood

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:

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.

Resources