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”:

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:

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.