LiveData related articles

LiveData is one of the most popular components of the Android Jetpack family. Right now, many modern Android applications use LiveData. In the previous article, we explored how LiveData works internally. Today, I want to share good practices of using LiveData<T> objects.

Use the "observe" method in the UI layer

LiveData has the observe and observeForever methods to observe the latest version of data stored in the LiveData<T> object. However, the observe method is the only lifecycle-aware method for monitoring data in the LiveData object.

public void observe(
    @NonNull LifecycleOwner owner, 
    @NonNull Observer<? super T> observer
)

When we use this method, we pass the LifecycleOwner parameter, and when the state of it is DESTROYED, the whole LiveData will be unsubscribed automatically from the lifecycle owner. It helps us to avoid memory leaks.

viewModel.products.observe(this) {
    handleProducts(it)
}

The AppCompatActivity and Fragment are the most popular LifecycleOwners.

Do not expose the MutableLiveData to the UI layer

The UI layer of the application is responsible for displaying data on the screen and processing User input to other layers of the application. This means that the UI shouldn't be able to change data directly in the LiveData object. The ViewModel, Presenter, Controller, or something similar should have an option to change data in the LiveData objects.

class ProductsViewModel(...) : ViewModel() {
    private val _products = MutableLiveData<List<Product>>()
    val products: LiveData<List<Product>>
        get() = _products

    ...
}

This implementation allows us to change the products using the _products variable only in the ProductsViewModel class. The read-only products property allows us only to observe data without modification.

Use LiveData only in the UI layer

LiveData is a data holder class. We can update the value of LiveData objects by using setValue and postValue methods. Under the hood, both of these methods set a value from the main thread which makes this structure ideal for the UI because all Views’ properties should be updated from the main thread.

The "setValue" vs "postValue" methods

You can read more about the internal work of LiveData here.

When we get data from data stores or want to do any calculation, we prefer to do it in a worker thread because it allows us to transform data in a worker thread, too. In this scenario, LiveData is not the best choice.

Use LiveData as a state

Usually, we handle multiple data objects for almost every screen of the application. Let's imagine we have a screen with a list of tasks. So, we can have one of the followings states:

  • Success
    • No tasks available
    • Tasks are available
  • Error
    • Connection error

In such a situation, we can have multiple LiveData objects in the ViewModel class.

class TasksViewModel(...) : ViewModel() {
    private val _tasks = MutableLiveData<List<Task>>()
    val tasks : LiveData<List<Task>>
        get() = _tasks

    private val _error = MutableLiveData<Throwable>()
    val error : LiveData<Throwable>
        get() = _error
        
    ...
}

I saw many more LiveData objects in a single ViewModel class. The main problem here is that we should handle LiveData in in a proper sequence:

  • Handle error state
  • Handle tasks

We can clean up the tasks when we have an error case, but we can handle it in a better way. The ViewModel can have only one LiveData object with the state of the whole screen:

private val _tasks = MutableLiveData<UiState<List<Task>>>()
val tasks : LiveData<UiState<List<Task>>>
    get() = _tasks

In this case, the UiState can be Success, Error or Loading.

sealed class UiState<out T : Any> {
    data class Success<out T: Any>(val data: T) : UiState<T>()
    data class Error(val error: Throwable) : UiState<Nothing>()
    object Loading : UiState<Nothing>()
}

In this case, we don't need any sequence for handling data because only the Success sealed class has access to notes and only the Error one has access to error details.

viewModel.tasks.observe(this) {
    when (it) {
        is UiState.Success -> handleSuccess(it.data)
        is UiState.Error -> handleError(it.error)
        is UiState.Loading -> showLoading()
    }
}

Remember about collecting latest emitted data after screen totation

The LiveData#observe method receives the latest version of the value after configuration changes, like screen rotation. So, we should remember it when we want to send data only once, like:

  • An error or warning message
  • A navigation event

If we’re going to get such data only once and avoid duplication events after configuration changes, we can use the SingleLiveEvent<T> instead of the MutableLiveData<T>.

However, the SingleLiveEvent<T> often called as anti-pattern and ideally we should avoid it. It can be replaced with the Flow.
You can read more about migrating from LiveData to StateFlow and SharedFlow" here.

However, if you still use the LiveData<T>, you can use the SingleLiveEvent<T>.

The implementation of the SingleLiveEvent<T> can be found here.

private val _message = SingleLiveEvent<String>()
val message : LiveData<String>
    get() = _message

...

_message.value = "Tasks successfully added"

Summary

The LiveData<T> is a commonly used structure for providing data to the UI layer. This structure is efficient for the UI layer because the postValue and setValue sets data internally from the main thread. You can read more about the internal work of LiveData here.

I recommend to apply the following tips:

  • Use the "observe" method in the UI layer
  • Use LiveData only in the UI layer
  • Use only one LiveData as a state in ViewModel
  • Use the SingleLiveEvent when you want to get data once
  • Use the LiveData for public variables in ViewModel, Presenter, Controller

Resources