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.xml
layout in which we use data binding - It has the
SensorLiveData
from theSensor/LiveList
sample, except that we no longer track the date of events, since we are not displaying that in the UI - It has a
SensorViewModel
that 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
SensorViewModel
from theViewModelProviders
- Attach the
SensorViewModel
to theMainBinding
by calling the generatedsetViewModel()
method - Supply the root view of the
main
layout tosetContentView()
- Observe the changes to the
SensorLiveData
, format each sensor reading into aString
representation, 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.