LiveData Updating Data Binding
Simply put: we want to be able to have LiveData update our data binding expressions. So, if we get new data from Room, or new data from a Sensor, or anything else that gives us a LiveData, we would like to have those changes be reflected in the UI, with as little effort as possible.
There are a few ways of going about this, outlined in the following sections. Each profiles a variation on the same sample app, which itself is a variation on the Sensor/LiveList sample from a previous chapter. In this case, the UI is a TextView showing the latest ambient light sensor reading. And, since we have covered ViewModel, we will use that for holding onto our SensorLiveData that is the source of those sensor readings. What varies between the three samples shown in this chapter is how those readings wind up affecting our UI via data binding.
Each of the three variations has the same “cast of characters”:
- We have an activity named
MainActivity - It has a
main.xmllayout in which we use data binding - It has the
SensorLiveDatafrom theSensor/LiveListsample, except that we no longer track the date of events, since we are not displaying that in the UI - It has a
SensorViewModelthat holds thatSensorLiveData
Updating Observables
The Sensor/SimpleBinding sample project uses an ObservableField to get the sensor readings into the layout. And, we have our own code to pipe events from the SensorLiveData into that ObservableField.
The SensorViewModel holds onto both the ObservableField and the SensorLiveData:
package com.commonsware.android.livedata;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.databinding.ObservableField;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.support.annotation.NonNull;
public class SensorViewModel extends AndroidViewModel {
public final SensorLiveData sensorLiveData;
public final ObservableField<String> sensorReading=new ObservableField<>();
public SensorViewModel(@NonNull Application app) {
super(app);
sensorLiveData=new SensorLiveData(app, Sensor.TYPE_LIGHT,
SensorManager.SENSOR_DELAY_UI);
}
}
We initialize the SensorLiveData in the constructor, using the Application supplied as an outcome of extending AndroidViewModel.
The main layout contains two TextView widgets: a label and our reading, wrapped in a ConstraintLayout:
<?xml version="1.0" encoding="utf-8"?>
<layout>
<data>
<variable
name="viewModel"
type="com.commonsware.android.livedata.SensorViewModel" />
</data>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/label_light"
android:textAppearance="@android:style/TextAppearance.Material.Large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.25" />
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@{viewModel.sensorReading}"
android:textAppearance="@android:style/TextAppearance.Material.Large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75" />
</android.support.constraint.ConstraintLayout>
</layout>
We have one data binding variable, named viewModel, which is our SensorViewModel instance. And, we have one binding expression, populating android:text of the second TextView with the sensorReading ObservableField.
MainActivity then glues the other two together:
package com.commonsware.android.livedata;
import android.arch.lifecycle.ViewModelProviders;
import android.os.Bundle;
import android.support.v4.app.FragmentActivity;
import com.commonsware.android.livedata.databinding.MainBinding;
public class MainActivity extends FragmentActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainBinding binding=MainBinding.inflate(getLayoutInflater());
SensorViewModel vm=ViewModelProviders.of(this).get(SensorViewModel.class);
binding.setViewModel(vm);
setContentView(binding.getRoot());
vm.sensorLiveData.observe(this, event ->
vm.sensorReading.set(String.format("%f", event.values[0])));
}
}
Here, we:
-
inflate()theMainBinding - Obtain our
SensorViewModelfrom theViewModelProviders - Attach the
SensorViewModelto theMainBindingby calling the generatedsetViewModel()method - Supply the root view of the
mainlayout tosetContentView() - Observe the changes to the
SensorLiveData, format each sensor reading into aStringrepresentation, andset()that value on theObservableField
The result is that as the SensorLiveData reports new readings, they get piped into the ObservableField, which triggers an update to the TextView.
Binding to LiveData
However, if the point of Observable is to provide updates to data to the data binding framework, and if the point of LiveData is to provide updates to data to observers… shouldn’t there be a way to make a LiveData be Observable?
In short: no.
However, that is not needed, because as of 2018, the data binding framework can work with LiveData directly. If your binding expressions reference LiveData objects, the data binding framework knows to observe those objects and use any updates to re-evaluate the binding expressions.
The only requirement is that we now have to provide a LifecycleOwner to our binding. There is a setLifecycleOwner() for this. That LifecycleOwner is used for observing the LiveData, and it should be a LifecycleOwner of relevance to the views being managed by the data binding framework. So, for an activity’s layout, you would use the activity as the LifecycleOwner.
The Sensor/LiveBinding sample project uses this approach.
SensorViewModel no longer has the ObservableField:
package com.commonsware.android.livedata;
import android.app.Application;
import android.arch.lifecycle.AndroidViewModel;
import android.hardware.Sensor;
import android.hardware.SensorManager;
import android.support.annotation.NonNull;
public class SensorViewModel extends AndroidViewModel {
public final SensorLiveData sensorLiveData;
public SensorViewModel(
@NonNull Application app) {
super(app);
sensorLiveData=new SensorLiveData(app, Sensor.TYPE_LIGHT,
SensorManager.SENSOR_DELAY_UI);
}
}
The binding expression now refers to sensorLiveData directly:
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginBottom="8dp"
android:layout_marginEnd="8dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@{viewModel.sensorLiveData}"
android:textAppearance="@android:style/TextAppearance.Material.Large"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintVertical_bias="0.75" />
onCreate() of MainActivity no longer needs to observe() the SensorLiveData itself, as the data binding framework will handle that. It does, however, need to call setLifecycleOwner():
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MainBinding binding=MainBinding.inflate(getLayoutInflater());
SensorViewModel vm=ViewModelProviders.of(this).get(SensorViewModel.class);
binding.setViewModel(vm);
binding.setLifecycleOwner(this);
setContentView(binding.getRoot());
}
In some cases, that is all that you will need. In this case, though, there are a couple of additional changes from the previous sample that are needed to make it work.
Our binding expression is attempting to populate the text of a TextView with the objects emitted by a SensorLiveData. Those are SensorLiveData.Event objects. The data binding framework needs the SensorLiveData and the SensorLiveData.Event classes to be public, as otherwise the generated MainBinding code cannot compile, since that code resides in a different package than does SensorLiveData itself.
Also, the data binding framework has no idea how to take a SensorLiveData.Event and use it to populate the text of a TextView. That requires a BindingAdapter:
@BindingAdapter("android:text")
public static void setLightReading(TextView tv, SensorLiveData.Event event) {
if (event==null) {
tv.setText(null);
}
else {
tv.setText(String.format("%f", event.values[0]));
}
}
Simply having this annotated static method in the project is sufficient; the data binding framework can find it on its own and know to apply it as needed.
That required BindingAdapter means that this project is a bit more complex than the previous one. However, it is cleaner, in that we are no longer needing to manage observing the LiveData ourselves. There are fewer places where we can screw up, particularly since a BindingAdapter is a static method and therefore should not be touching any state beyond whatever parameters are passed in.
Prev Table of Contents Next
This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.