Binding Your Data

So far, to update our UI, we have been pushing data into widgets from our Java or Kotlin code. For example, we have been updating the text property of a TextView.

Data binding, in general, refers to frameworks or libraries designed to pull data into the UI. UI definitions — such as Android layout resources — contain information about how to populate widgets from supplied model objects.

This chapter explores Android’s data binding support and how to use it to perhaps simplify your Android app development. And, we will also see why the word “perhaps” is in that previous sentence.

The Basic Steps

The DataBind sample module in the Sampler and SamplerJ projects adds to the InstanceState sample from the preceding chapter. The primary difference is that we will fill in the RecyclerView rows using data binding.

Enabling Data Binding

Data binding is an opt-in feature, in part because it can slow down the build process. For small projects like the ones in this book, you will not notice the overhead of the data binding tools, but that overhead becomes more annoying as projects get larger.

To opt into data binding, we need to enable it, by adding another closure to our module’s build.gradle file’s android closure:

  buildFeatures {
    dataBinding = true
    viewBinding true
  }

This works similar to how we enabled view binding earlier in the book. In this case, we are enabling both view binding and data binding.

Also, Kotlin projects using data binding should add the kotlin-kapt plugin:

apply plugin: 'kotlin-kapt'

This plugin enables annotation processing for Kotlin source files. Using annotations is not absolutely required in a data binding project, but it is somewhat common, and this plugin is needed so the data binding code can process those annotations.

Augmenting the Layout

The real fun begins with the layout for our RecyclerView row. The original edition of this layout resource was a typical ConstraintLayout with our TextView widgets:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
  xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  xmlns:tools="http://schemas.android.com/tools"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:padding="@dimen/content_padding">

  <TextView
    android:id="@+id/activityHash"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:text="0x12345678" />

  <TextView
    android:id="@+id/viewmodelHash"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toBottomOf="@id/activityHash"
    tools:text="0x90ABCDEF" />

  <androidx.constraintlayout.widget.Barrier
    android:id="@+id/barrier"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginEnd="4dp"
    android:layout_marginStart="4dp"
    app:barrierDirection="start"
    app:constraint_referenced_ids="activityHash,viewmodelHash" />

  <TextView
    android:id="@+id/timestamp"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    tools:text="01:23" />

  <TextView
    android:id="@+id/message"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:textAppearance="?android:attr/textAppearanceLarge"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toStartOf="@id/barrier"
    app:layout_constraintStart_toEndOf="@id/timestamp"
    app:layout_constraintTop_toTopOf="parent"
    tools:text="onDestroy()" />

</androidx.constraintlayout.widget.ConstraintLayout>

We need to make some changes to that in order to leverage data binding:

<?xml version="1.0" encoding="utf-8"?>
<layout>

  <data>

    <import type="android.text.format.DateUtils" />

    <variable
      name="event"
      type="com.commonsware.jetpack.sampler.databind.Event" />

    <variable
      name="elapsedSeconds"
      type="Long" />
  </data>

  <androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:padding="@dimen/content_padding">

    <TextView
      android:id="@+id/activityHash"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{Integer.toHexString(event.activityHash)}"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      tools:text="0x12345678" />

    <TextView
      android:id="@+id/viewmodelHash"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{Integer.toHexString(event.viewmodelHash)}"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toEndOf="parent"
      app:layout_constraintTop_toBottomOf="@id/activityHash"
      tools:text="0x90ABCDEF" />

    <androidx.constraintlayout.widget.Barrier
      android:id="@+id/barrier"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:layout_marginEnd="4dp"
      android:layout_marginStart="4dp"
      app:barrierDirection="start"
      app:constraint_referenced_ids="activityHash,viewmodelHash" />

    <TextView
      android:id="@+id/timestamp"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{DateUtils.formatElapsedTime(elapsedSeconds)}"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintStart_toStartOf="parent"
      app:layout_constraintTop_toTopOf="parent"
      tools:text="01:23" />

    <TextView
      android:id="@+id/message"
      android:layout_width="wrap_content"
      android:layout_height="wrap_content"
      android:text="@{event.message}"
      android:textAppearance="?android:attr/textAppearanceLarge"
      app:layout_constraintBottom_toBottomOf="parent"
      app:layout_constraintEnd_toStartOf="@id/barrier"
      app:layout_constraintStart_toEndOf="@id/timestamp"
      app:layout_constraintTop_toTopOf="parent"
      tools:text="onDestroy()" />

  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

First, the entire resource file gets wrapped in a <layout> element, on which we can place the android namespace declaration. Only layout resources with a root <layout> element are processed by the data binding portion of the build system.

That <layout> element then has two children. The second child is our ConstraintLayout, representing the root View or ViewGroup for the resource. The first child is a <data> element, and that is where we configure how data binding should proceed when this layout resource gets used.

Inside of <data> we have three elements.

Two are <variable> elements. These identify and describe the objects that we can pull data from. In our case, we have an event variable that is an instance of our Event model class, and we have an elapsedSeconds value that represents the number of seconds that have elapsed between the start of the app and this event. As we will see, our Java or Kotlin code will now “bind” those objects into our layout, rather than update widgets directly.

The other element is <import>. This works like import statements in Java or Kotlin, indicating a particular class that we would like to reference. In the world of data binding, mostly we use <import> for classes where we wish to refer to static methods or fields (and their Kotlin equivalents).

Our four TextView widgets now have android:text attributes, where they had none before. That is because the earlier version of this sample relied upon Java or Kotlin code to push data into the widgets, and now we are going to pull data in using data binding.

Those android:text attributes use “binding expressions”. A binding expression is identified in a data binding-enhanced layout through @{} syntax, with the actual expression between the braces. Those expressions use a language syntax that looks a lot like Java or Kotlin expressions, and they can reference:

The activityHash TextView has android:text="@{Integer.toHexString(event.activityHash)}". The binding expression works like its Java or Kotlin counterparts, taking our activityHash value out of our event and formatting it as a hex string. The text of this TextView will then contain that hex string.

The android:text attributes of the other TextView widgets work the same:

While we happen to use android:text for all four widgets, binding expressions can be applied to just about any attribute, such as android:checked for the checked state of a CompoundButton.

Updating the Model

Data binding has its limits. One limit is that it can only access public functions and properties. So, we need to ensure that everything we need is now public in our Event class, both in Java:

package com.commonsware.jetpack.samplerj.databind;

import android.os.Parcel;
import android.os.Parcelable;
import android.os.SystemClock;

public class Event implements Parcelable {
  public final long timestamp;
  public final String message;
  public final int activityHash;
  public final int viewmodelHash;

  Event(String message, int activityHash, int viewmodelHash) {
    this.message = message;
    this.activityHash = activityHash;
    this.viewmodelHash = viewmodelHash;
    this.timestamp = SystemClock.elapsedRealtime();
  }

  protected Event(Parcel in) {
    timestamp = in.readLong();
    message = in.readString();
    activityHash = in.readInt();
    viewmodelHash = in.readInt();
  }

  @Override
  public int describeContents() {
    return 0;
  }

  @Override
  public void writeToParcel(Parcel dest, int flags) {
    dest.writeLong(timestamp);
    dest.writeString(message);
    dest.writeInt(activityHash);
    dest.writeInt(viewmodelHash);
  }

  @SuppressWarnings("unused")
  public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
    @Override
    public Event createFromParcel(Parcel in) {
      return new Event(in);
    }

    @Override
    public Event[] newArray(int size) {
      return new Event[size];
    }
  };
}

…and Kotlin:

package com.commonsware.jetpack.sampler.databind

import android.os.Parcelable
import android.os.SystemClock
import kotlinx.android.parcel.Parcelize

@Parcelize
data class Event(
  val message: String,
  val activityHash: Int,
  val viewmodelHash: Int,
  val timestamp: Long = SystemClock.elapsedRealtime()
) : Parcelable

Applying the Binding

The layout configures one side of the binding: pulling data into widgets. We still need to do some work to configure the other side of the binding: supplying the source of that data. In the case of this example, we need to provide the Event object for this layout resource.

That is handled via some modifications to our EventAdapter and EventViewHolder, to get at a “binding object” for an inflated layout and then call functions on it to supply our variables.

Creating the Binding

When we use <layout> in a layout resource and set up the layout side of the data binding system, the build system code-generates a Java class associated with that layout file. As with view binding, the class name is derived from the layout name, where names_like_this get converted into NamesLikeThis and have Binding appended. So, since our layout resource was row.xml, we get RowBinding. This is code-generated into a databinding Java sub-package of the package name from the manifest. Hence, the fully-qualified import statement for this class is:

import com.commonsware.jetpack.sampler.databind.databinding.RowBinding;

(normally, projects would not have databind in their own application IDs, so the near-duplication that you see here is peculiar to this project)

This is a subclass of ViewDataBinding, supplied by the androidx.databinding libraries that are added to your project by enabling data binding in your build.gradle file.

Creating an instance of the binding also inflates the associated layout. Your binding class has a number of factory methods for inflating the layout and creating the binding. These mirror other methods that you have used elsewhere:

Our revised version of EventAdapter uses the three-parameter flavor of inflate(), which takes a LayoutInflater (obtained from the hosting activity), the parent container, and false. This mirrors the inflate() one would use on LayoutInflater itself, except that it also gives us our binding. We use this in onCreateViewHolder() and pass the RowBinding into our EventViewHolder:

  @NonNull
  @Override
  public EventViewHolder onCreateViewHolder(@NonNull ViewGroup parent,
                                            int viewType) {
    RowBinding binding = RowBinding.inflate(inflater, parent, false);

    return new EventViewHolder(binding, startTime);
  }
  override fun onCreateViewHolder(
    parent: ViewGroup,
    viewType: Int
  ): EventViewHolder {
    val binding = RowBinding.inflate(inflater, parent, false)

    return EventViewHolder(binding, startTime)
  }

Pouring the Model into the Binding

The generated binding class will have setters for each <variable> in our <data> element in the layout. Setter names are generated from the variable names using standard JavaBean conventions, so our event variable becomes setEvent() and our elapsedSeconds variable becomes setElapsedSeconds(). When we call setEvent() and setElapsedSeconds(), the generated code will use those objects to populate our TextView widgets, applying the binding expression from our android:text attributes.

To accomplish this, our revised EventViewHolder holds onto the RowBinding and uses it in bindTo():

package com.commonsware.jetpack.samplerj.databind;

import com.commonsware.jetpack.samplerj.databind.databinding.RowBinding;
import androidx.recyclerview.widget.RecyclerView;

class EventViewHolder extends RecyclerView.ViewHolder {
  private final long startTime;
  private final RowBinding row;

  EventViewHolder(RowBinding row, long startTime) {
    super(row.getRoot());

    this.row = row;
    this.startTime = startTime;
  }

  void bindTo(Event event) {
    row.setElapsedSeconds((event.timestamp - startTime)/1000);
    row.setEvent(event);
    row.executePendingBindings();
  }
}
package com.commonsware.jetpack.sampler.databind

import androidx.recyclerview.widget.RecyclerView
import com.commonsware.jetpack.sampler.databind.databinding.RowBinding

class EventViewHolder(val row: RowBinding, private val startTime: Long) :
  RecyclerView.ViewHolder(row.root) {
  fun bindTo(event: Event) {
    row.elapsedSeconds = (event.timestamp - startTime) / 1000
    row.event = event
    row.executePendingBindings()
  }
}

We also call executePendingBindings() on the RowBinding. This is needed when populating items in a RecyclerView, to ensure that those binding expressions are evaluated and applied immediately rather than a bit later. In cases not involving RecyclerView, though, it is usually safe to skip the executePendingBindings() call.

Getting the Root View

When we chain to the superclass constructor in a RecyclerView.ViewHolder, we need to pass the View that is the UI for that particular bit of UI that the ViewHolder is managing. To get the root view of a layout associated with a binding object, call getRoot() on the binding object, as we do with view binding. That’s what EventViewHolder does, passing the getRoot() results to the RecyclerView.ViewHolder constructor.

Results

Visually, this app is the same as before. Functionally, the app is the same as before. And, from a code complexity standpoint, the app is probably worse than before, as we went through a lot of work just to avoid calling findViewById() and setText() a few times.


Prev Table of Contents Next

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