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.
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 theFlow
.
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