LiveData and Data Binding

Android’s data binding framework offers a way for you to push data into your widgets with less code, by “binding” sources of data to those widgets via special XML attributes in layout resources. The data binding framework was created before the Architecture Components existed, and so originally the data binding framework had no support for LiveData. Now, though, it does, and so you can have your UI update itself automatically from LiveData sources, if you choose.

In this chapter, we will explore how to set this up.

A Data Binding Recap

Extensive coverage of data binding can be found in The Busy Coder’s Guide to Android Development. However, here is a brief reminder of what data binding is and how it works.

Note that data binding is not automatically used in an Android project. You need to enable it via dataBinding { enabled true } in the android closure of your module’s build.gradle file.

New Layout Resource Structure

Classic Android layout resources are purely a view hierarchy. Typically, the root XML element then is the outermost container class (e.g., a ConstraintLayout).

A layout resource that will participate in data binding will instead have a root <layout> element. That will have two child elements:

Those need to appear in that order, with <data> as the first child.

A <data> element can have a variety of child elements to configure the data binding, but the most important ones are <variable> elements. These declare what objects are being bound to this layout, and they provide both the name and the Java/Kotlin type of the object:

<layout>

  <data>

    <variable
      name="viewModel"
      type="com.commonsware.android.livedata.SensorViewModel" />
  </data>

  <!-- view hierarchy goes here -->
</layout>

Binding Expressions

The XML attributes of the views can then reference those variables in binding expressions. Rather than having a simple value (e.g., android:text="Hello, world!"), a binding expression contains references to variables (plus public fields and methods on those variables) and other view attributes, with a simple expression syntax. The data binding framework identifies these expressions via @{} syntax (android:text="@{viewModel.sensorReading}").

Adapters

Sometimes, the data types available to binding expressions do not quite match the data type needed by the XML attribute. For example, android:text maps to setText() on TextView, and that needs either an int resource ID or a CharSequence (e.g., String) value. If you have something else — such as a SensorLiveData.Event object — you will need to provide a “binding adapter” to help bridge the gap.

These adapters come in the form of static methods annotated with @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]));
  }
}

The annotation argument is the name of the XML attribute to which this adapter applies. So, in this case, if we try binding a SensorLiveEvent.Data object into a TextView via its android:text attribute, this method will be called. It is up to us then to do something useful to fulfill that binding — in this case, we call setText() on the TextView with a suitable value, based on the event.

Binding from Code

Somewhere, though, we need to supply the values for those variables. That comes from using a different way to set up this layout. Rather than using setContentView() or a LayoutInflater directly, we use a code-generated class, created by the tools associated with the data binding framework. This class not only takes care of inflating the layout for us, but it gives us setter methods to supply the variables.

The name of this code-generated class is based on the name of the layout resource:

So, main.xml turns into MainBinding, activity_main.xml turns into ActivityMainBinding, and so on.

The binding class has a static method named inflate() that takes a LayoutInflater and returns an instance of the binding class (like a factory method would), inflating the underlying layout along the way:

MainBinding binding=MainBinding.inflate(getLayoutInflater());

It has a getRoot() method that returns the root View of your view hierarchy, for use with setContentView(), onCreateViewHolder(), etc. And it has methods for each one of your variables, where the method name is derived from the variable name:

So, a viewModel variable results in a setViewModel() method. Calling this on the binding triggers the evaluation of the binding expressions and populates the attributes of the affected widgets.

Observable Data Sources

If the variables implement the Observable interface, then once you have attached the variables’ objects to the binding, changes to the variables’ values automatically re-evaluate the binding expressions. You do not need to do anything else.

For simple primitives, there are a series of Observable implementations, such as ObservableBoolean and ObservableInt. For String and arbitrary other objects, there is a generic ObservableField that you can use.

We will see an example of this shortly, as this is where LiveData starts becoming important.


Prev Table of Contents Next

This book is licensed under the Creative Commons Attribution-ShareAlike 4.0 International license.