Android’s LiveData is a valuable asset in wrinting easy to maintain software. It introduces a life-cycle for data updates that help to reduce boilerplate code and make user interfaces more flexible. Unfortunately LiveData only covers one data point. If you need more data in your UI, you’ll face the problem that you see each update separately and therefore have unnecessary updates of your UI. Here I’ll show you how to get rid of those by updating the UI with atomic updates.
Introduction: LiveData basics
LiveData follows the observer pattern which was described over 25 years ago: you have a mutable object and you get informed when this object changes. What makes this flexible is that there can be zero or many observers on the object. This allows loose coupling of the components of an application.
When it comes to data updates, concurrency is always something to pay attention to. On Android there is a special thread called the main or UI thread. All UI updates happen on it as well as the handling of all UI events like button presses. It is therefore imperative that you don’t block this thread for too long and it is prohibited to do network IO on it. LiveData notifies its observers on the main thread as well. It therefore offers two methods that are important for multi-threading:
setValue(x)
setValue(x)
postValue(x)
postValue(x)
The difference is that
setValue
setValue
may only be called on the main thread but changes the value immediately while
postValue
postValue
may be called on any thread and puts the update to the value into the queue on the main thread. This means that any changes via
postValue
postValue
do not take effect immediately but delayed. If you run this code:
liveData.getValue(); // returns 1
liveData.getValue(); // still returns 1!
liveData.setValue(1);
liveData.getValue(); // returns 1
liveData.postValue(2);
liveData.getValue(); // still returns 1!
liveData.setValue(1);
liveData.getValue(); // returns 1
liveData.postValue(2);
liveData.getValue(); // still returns 1!
then both calls to
getValue
getValue
will return 1 as the change to 2 has not happened yet on the second call.
If you run this code
liveData.postValue(2);
liveData.setValue(1);
liveData.postValue(2);
liveData.setValue(1);
then every observer will first get the value 1 and then the value 2.
What happens if you update it several times? This code (which can only run on the main thread)
liveData.setValue(1);
liveData.setValue(2);
liveData.setValue(1);
liveData.setValue(2);
will call each observer twice (immediately), first with 1 and then with 2, while this code (which can run on any thread)
liveData.postValue(1);
liveData.postValue(2);
liveData.postValue(1);
liveData.postValue(2);
will call the observers only once with the value 2 after everything is done on this call from the main thread.
The problem
Imagine you have a UI that shows two different items. Let’s say it is a map that has a location and a zoom level. Both of these values can change independently. They are also unrelated in the sense, that each can live without the other. You could model them into one object
LocationAndZoom
LocationAndZoom
, but this would violate the
single responsibility principle. So you would model them with two independent LiveData:
LiveData<Location> liveLocation;
LiveData<Location> liveLocation;
LiveData<Zoom> liveZoom;
LiveData<Location> liveLocation;
LiveData<Zoom> liveZoom;
You want your UI to update whenever one of them changes, but you need both values to paint the map.
liveLocation.observe(this, t -> updateMap());
liveZoom.observe(this, s -> updateMap());
liveLocation.observe(this, t -> updateMap());
liveZoom.observe(this, s -> updateMap());
liveLocation.observe(this, t -> updateMap());
liveZoom.observe(this, s -> updateMap());
Now picture that after a search you want you to update both location and zoom level. So you would do
liveLocation.postValue(searchResult.getLocation());
liveZoom.postValue(ZOOM_CITY);
liveLocation.postValue(searchResult.getLocation());
liveZoom.postValue(ZOOM_CITY);
liveLocation.postValue(searchResult.getLocation());
liveZoom.postValue(ZOOM_CITY);
This results in two calls to
updateMap()
updateMap()
, one with stale and one with current data. As map redraws are expensive, we should optimize it!
The solution
So you want to get only one update no matter how many of your UI variables change at the same time. For this you need another LiveData with an object that consolidates all data that you need:
private static class LocationAndZoom {
private final Location location;
private LocationAndZoom(Location location, Zoom zoom) {
this.location = location;
private static class LocationAndZoom {
private final Location location;
private final Zoom zoom;
private LocationAndZoom(Location location, Zoom zoom) {
this.location = location;
this.zoom = zoom;
}
}
private static class LocationAndZoom {
private final Location location;
private final Zoom zoom;
private LocationAndZoom(Location location, Zoom zoom) {
this.location = location;
this.zoom = zoom;
}
}
This is not a violation of the single responsibility principle as a change to it is driven by the same stake holder („and now we need humidity as well in the UI“). Now we can define a LiveData that holds this
LocationAndZoom
LocationAndZoom
. But how can we achieve that it only notifies its observers once even when there are multiple updates? We will use the fact that
postValue
postValue
postpones updates to the next run of the main thread. This is the idea:
1st run of the main thread
Both LiveData will be updated:
liveLocation.postValue(searchResult.getLocation());
liveZoom.postValue(ZOOM_CITY);
liveLocation.postValue(searchResult.getLocation());
liveZoom.postValue(ZOOM_CITY);
liveLocation.postValue(searchResult.getLocation());
liveZoom.postValue(ZOOM_CITY);
2nd run of the main thread
This results in two calls to the observeable which will update the consolidated LiveData:
// from liveLocation observer:
liveLocationAndZoom.postValue(new LocationAndZoom(...)); // <= here the zoom is stale
// from liveZoom observer
liveLocationAndZoom.postValue(new LocationAndZoom(...)); // <= here both are recent
// from liveLocation observer:
liveLocationAndZoom.postValue(new LocationAndZoom(...)); // <= here the zoom is stale
// from liveZoom observer
liveLocationAndZoom.postValue(new LocationAndZoom(...)); // <= here both are recent
// from liveLocation observer:
liveLocationAndZoom.postValue(new LocationAndZoom(...)); // <= here the zoom is stale
// from liveZoom observer
liveLocationAndZoom.postValue(new LocationAndZoom(...)); // <= here both are recent
Certainly not like this in the code, but you should get the point that there are two updates.
3rd run of the main thread
As both calls are
postValue
postValue
, only the last one wins and resulting in just one call to the observables of
liveLocationAndZoomd
liveLocationAndZoomd
with the recent value.
Implementation
The MediatorLiveData allows to change LiveData objects on the fly. This is used to change from the source changes to the consolidated change. To simplify the code use this class:
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.Observer;
public abstract class Merger<T> {
private MediatorLiveData<T> liveMerged;
protected abstract boolean isValid(final T merged);
public synchronized LiveData<T> getMerged() {
if (null == liveMerged) {
liveMerged = new MediatorLiveData<>();
protected abstract void init();
protected <S> void addSource(
@NonNull LiveData<S> source, @NonNull Observer<? super S> observer) {
liveMerged.addSource(source, observer);
protected T getCurrentMerged() {
protected void updateCurrent(T newMerged) {
currentMerged = newMerged;
if (isValid(currentMerged)) {
liveMerged.postValue(currentMerged);
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.Observer;
public abstract class Merger<T> {
private MediatorLiveData<T> liveMerged;
private T currentMerged;
protected abstract boolean isValid(final T merged);
public synchronized LiveData<T> getMerged() {
if (null == liveMerged) {
liveMerged = new MediatorLiveData<>();
init();
}
return liveMerged;
}
protected abstract void init();
protected <S> void addSource(
@NonNull LiveData<S> source, @NonNull Observer<? super S> observer) {
liveMerged.addSource(source, observer);
}
protected T getCurrentMerged() {
return currentMerged;
}
protected void updateCurrent(T newMerged) {
currentMerged = newMerged;
if (isValid(currentMerged)) {
liveMerged.postValue(currentMerged);
}
}
}
import androidx.annotation.NonNull;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MediatorLiveData;
import androidx.lifecycle.Observer;
public abstract class Merger<T> {
private MediatorLiveData<T> liveMerged;
private T currentMerged;
protected abstract boolean isValid(final T merged);
public synchronized LiveData<T> getMerged() {
if (null == liveMerged) {
liveMerged = new MediatorLiveData<>();
init();
}
return liveMerged;
}
protected abstract void init();
protected <S> void addSource(
@NonNull LiveData<S> source, @NonNull Observer<? super S> observer) {
liveMerged.addSource(source, observer);
}
protected T getCurrentMerged() {
return currentMerged;
}
protected void updateCurrent(T newMerged) {
currentMerged = newMerged;
if (isValid(currentMerged)) {
liveMerged.postValue(currentMerged);
}
}
}
now you can implement the consolidates LiveData like this:
private static class LocationAndZoomMerger extends Merger<LocationAndZoom> {
private final LiveData<Location> liveLocation;
private final LiveData<Zoom> liveZoom;
private LocationAndZoomMerger(final LiveData<Location> liveLocation,
final LiveData<Zoom> liveZoom) {
this.liveLocation = liveLocation;
this.liveZoom = liveZoom;
updateCurrent(new LocationAndZoom(null, null));
l -> updateCurrent(new LocationAndZoom(l, getCurrentMerged().zoom)));
z -> updateCurrent(new LocationAndZoom(getCurrentMerged().location, z)));
protected boolean isValid(final LocationAndZoom locationAndZoom) {
return null != locationAndZoom.location && null != locationAndZoom.zoom;
private static class LocationAndZoomMerger extends Merger<LocationAndZoom> {
private final LiveData<Location> liveLocation;
private final LiveData<Zoom> liveZoom;
private LocationAndZoomMerger(final LiveData<Location> liveLocation,
final LiveData<Zoom> liveZoom) {
this.liveLocation = liveLocation;
this.liveZoom = liveZoom;
}
@Override
protected void init() {
updateCurrent(new LocationAndZoom(null, null));
addSource(
liveLocation,
l -> updateCurrent(new LocationAndZoom(l, getCurrentMerged().zoom)));
addSource(
liveZoom,
z -> updateCurrent(new LocationAndZoom(getCurrentMerged().location, z)));
}
@Override
protected boolean isValid(final LocationAndZoom locationAndZoom) {
return null != locationAndZoom.location && null != locationAndZoom.zoom;
}
}
private static class LocationAndZoomMerger extends Merger<LocationAndZoom> {
private final LiveData<Location> liveLocation;
private final LiveData<Zoom> liveZoom;
private LocationAndZoomMerger(final LiveData<Location> liveLocation,
final LiveData<Zoom> liveZoom) {
this.liveLocation = liveLocation;
this.liveZoom = liveZoom;
}
@Override
protected void init() {
updateCurrent(new LocationAndZoom(null, null));
addSource(
liveLocation,
l -> updateCurrent(new LocationAndZoom(l, getCurrentMerged().zoom)));
addSource(
liveZoom,
z -> updateCurrent(new LocationAndZoom(getCurrentMerged().location, z)));
}
@Override
protected boolean isValid(final LocationAndZoom locationAndZoom) {
return null != locationAndZoom.location && null != locationAndZoom.zoom;
}
}
Finally observe the merged LiveData and enjoy atomic updates:
new LocationAndZoomMerger(liveLocation, liveZoom)
.observe(this, m -> Log.e("Atomic", "update"));
new LocationAndZoomMerger(liveLocation, liveZoom)
.getMerged()
.observe(this, m -> Log.e("Atomic", "update"));
new LocationAndZoomMerger(liveLocation, liveZoom)
.getMerged()
.observe(this, m -> Log.e("Atomic", "update"));