Codepath

Applying Data Binding for Views

Overview

Android has now released a stable data-binding library which allows you to connect views with data in a much more powerful way than was possible previously. Applying data binding can improve your app by removing boilerplate for data-driven UI and allowing for two-way binding between views and data objects.

The Data Binding Library is an Android Jetpack library that is compatible with all recent Android versions. See this official video from Google for a brief overview.

Setup

To get started with data binding, we need to make sure to upgrade to the latest version of the Android Gradle plugin.

To configure your app to use data binding, add the dataBinding element to your app/build.gradle file:

plugins {
    id: 'com.android.application'
    id: 'kotlin-kapt' // If using Kotlin
}

android {
    // ...
    buildFeatures {
        dataBinding true
    }
}

Eliminating View Lookups

The most basic thing we get with data binding is the elimination of findViewById. To enable this to work for a layout file, first we need to change the layout file by making the outer tag <layout> instead of whatever ViewGroup you use (note that the XML namespaces should also be moved):

<layout xmlns:android="http://schemas.android.com/apk/res/android"
        xmlns:tools="http://schemas.android.com/tools">
    <RelativeLayout
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:paddingLeft="@dimen/activity_horizontal_margin"
            android:paddingRight="@dimen/activity_horizontal_margin"
            android:paddingTop="@dimen/activity_vertical_margin"
            android:paddingBottom="@dimen/activity_vertical_margin"
            tools:context=".MainActivity">

        <TextView
                android:id="@+id/tvLabel"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"/>

    </RelativeLayout>
</layout>

The layout container tag tells Android Studio that this layout should take the extra processing during compilation time to find all the interesting Views and note them for use with the data binding library.

Essentially creating this outer layout tag causes a special reserved class file to be generated at compile time based on the name of the file. For instance, activity_main.xml will generate a class called ActivityMainBinding. Although this class is not generated at compile-time, it can still be referenced in Android Studio thanks to integrated support for data binding.

Now, we can use the Binding object to access the view. In our activity, we can now inflate the layout content using the binding:

public class MainActivity extends AppCompatActivity {
    // Store the binding
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Inflate the content view (replacing `setContentView`)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // Store the field now if you'd like without any need for casting
        TextView tvLabel = binding.tvLabel;
        tvLabel.setAllCaps(true);
        // Or use the binding to update views directly on the binding
        binding.tvLabel.setText("Foo");
    }
}
class MainActivity : AppCompatActivity() {
    // Store the binding
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Inflate the content view (replacing `setContentView`)
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        // Store the field now if you'd like without any need for casting
        val tvLabel: TextView = binding.tvLabel
        tvLabel.isAllCaps = true
        // Or use the binding to update views directly on the binding
        binding.tvLabel.text = "Foo"
    }
}

Note that this binding object is generated automatically with the following rules in place:

  • Layout file activity_main.xml becomes ActivityMainBinding binding object
  • View IDs within a layout become variables inside the binding object (i.e binding.tvLabel)

The binding process makes a single pass on all views in the layout to assign the views to the fields. Since this only happens once, this can actually be faster than calling findViewById.

Refer to this guide for more details about bindings and inflating views.

One Way Data Binding

The simplest binding is to automatically load data from an object into a view directly using a new syntax in the template. For example, suppose we want to display a user's name inside a TextView. Assuming a simple user class:

public class User {
   public String firstName;
   public String lastName;
}
data class User(
    val firstName: String,
    val lastName: String
)

Next, we need to wrap our existing layout inside a <layout> tag to indicate we want data binding enabled:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <RelativeLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:paddingLeft="@dimen/activity_horizontal_margin"
        android:paddingRight="@dimen/activity_horizontal_margin"
        android:paddingTop="@dimen/activity_vertical_margin"
        android:paddingBottom="@dimen/activity_vertical_margin"
        tools:context=".MainActivity">

        <TextView
            android:id="@+id/tvFullName"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Hello World" />

    </RelativeLayout>
</layout>

Next, we need to indicate that we want to load data from a particular object by declaring variable nodes in a data section of the <layout>:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
    <data>
       <variable name="user" type="com.example.User"/>
    </data>
    <!-- ... rest of layout here -->
</layout>

The user variable within the data block describes a property that can now be used within this layout. Next, we can now reference this object inside of our views using @{variable.field} syntax such as:

<TextView
    android:id="@+id/tvFullName"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text='@{user.firstName + " " + user.lastName}' />

We can use conditional logic and other operations as part of the binding expression language for more complex cases. Finally, we need to assign that user data variable to the binding at runtime:

public class MainActivity extends AppCompatActivity {
    private ActivityMainBinding binding;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Inflate the `activity_main` layout
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        // Create or access the data to bind
        User user = new User("Sarah", "Gibbons");
        // Attach the user to the binding
        binding.setUser(user);
    }
}
class MainActivity : AppCompatActivity() {
    private lateinit var binding: ActivityMainBinding

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Inflate the `activity_main` layout
        binding = DataBindingUtil.setContentView(this, R.layout.activity_main)
        // Create or access the data to bind
        val user = User("Sarah", "Gibbons")
        // Attach the user to the binding
        binding.user = user
    }
}

That's all! When running the app now, you'll see the name is populated into the layout automatically:

Controlling visibility

You can use binding expressions to control the visibility of an item.

First, make sure you have access to the View properties (i.e. View.INVISIBLE and View.GONE) by importing the object into the template.

<data>
   <import type="android.view.View" />
</data>

Next, you can test for a condition (i.e. first name is null) and determine whether to show or hide the text view accordingly:

<TextView
   android:visibility="@{user.firstName != null ? View.VISIBLE: View.GONE}">

</TextView>

Image Loading

Unlike TextViews, you cannot bind values directly. In this case, you need to create a custom attribute.

First, make sure to define the res-auto namespace in your layout. You cannot declare it in the <layout> outer tag, so it should be placed in the outermost tag after the <data> tag:

<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">
  
  <data>
    <variable
      name="mymodel"
      type="com.example.MyApp.Model">
    </variable>
  </data>

  <RelativeLayout xmlns:app="http://schemas.android.com/apk/res-auto">
  </RelativeLayout>

</layout>

and:

<ImageView app:imageUrl=“@{mymodel.imageUrl}”>

You then need to annotate a static method that maps the custom attribute:

public class BindingAdapterUtils {
  @BindingAdapter({"imageUrl"})
  public static void loadImage(ImageView view, String url) {
     Picasso.get().load(url).into(view); 
  }
}
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String?) {
    Picasso.get().load(url).into(view)
}

Event Handling

Data Binding allows you to write expressions handling events that are dispatched from the views (e.g. onClick). To attach events to views, we can use method references or listener bindings.

Using Data Binding inside RecyclerView

We first need to modify the ViewHolder class to include a reference to the data binding class:

public class SamplesViewHolder extends RecyclerView.ViewHolder {
  final ItemUserBinding binding;  // this will be used by onBindViewHolder()

  public ItemViewHolder(View rootView) {
     super(rootView);
   
     // Since the layout was already inflated within onCreateViewHolder(), we 
     // can use this bind() method to associate the layout variables
     // with the layout.
     binding = ItemUserBinding.bind(rootView);

  }
}
inner class SamplesViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    // Since the layout was already inflated within onCreateViewHolder(), we 
    // can use this bind() method to associate the layout variables
    // with the layout.
    val binding: ItemUserBinding = ItemUserBinding.bind(itemView)
}

Next, we modify the onBindViewHolder() to associate the User object with the user at the given position and then update the views with the newly bound references:

@Override 
   public void onBindViewHolder(BindingHolder holder, int position) { 
      final User user = users.get(position); 

      // add these lines
      holder.binding.setUser(user);  // setVariable(BR.user, user) would also work
      holder.binding.executePendingBindings();   // update the view now
   } 
override fun onBindViewHolder(holder: BindingHolder, position: Int) {
      val user = users.get[position] 

      // add these lines
      holder.binding.user = user  // setVariable(BR.user, user) would also work
      holder.binding.executePendingBindings()   // update the view now
   } 

Refer also to these tutorials for how to work with data binding in RecyclerView or ListView:

Data Binding and the Include Tag

Refer to the following resources related to the include tag and binding:

Two Way Data Binding

If you want to have a two-way binding between the view and the data source, check out this handy 2-way data binding tutorial. You can also check this reference on 2-way data binding and this related post on inverse functions.

Advanced Data Binding

There is a great series of posts outlining advanced data binding features with the most important highlighted below:

Data Binding Best Practices

While MVVM with Data Binding does remove a good deal of this boilerplate you also run into new issues where logic is now present in the views, like this code similar to the section shown above:

<TextView 
    ...
    android:visibility="@{post.hasComments ? View.Visible : View.Gone}" />

This now buries the logic in an Android XML view and its near impossible to test. Instead, we should use a utility attribute:

public class BindingAdapterUtils {
  @BindingAdapter({"isVisible"})
  public static void setIsVisible(View view, boolean isVisible) {
      if (isVisible) {
        view.visibility = View.VISIBLE
      } else {
        view.visibility = View.GONE
      }
  }
}
@BindingAdapter("isVisible")
fun setIsVisible(view: View, isVisible: Boolean) {
  if (isVisible) {
    view.visibility = View.VISIBLE
  } else {
    view.visibility = View.GONE
  }
}

The logic for showing a view is now determined by a Boolean value. To use this in an MVVM Data Binding XML View you’d do the following in your view:

<TextView 
    ...
    app:isVisible="@{post.hasComments()}" />

For more best practices, check out this source article here.

MVVM Architecture and Data Binding

The Data binding framework can be used in conjunction with an MVVM architecture to help to decouple the View from the Model. In this approach, the binding framework connects with the ViewModel, which exposes data from the Model, into the View (xml layout).

Check out this blog post for a detailed overview.

Troubleshooting

Issues with the Binding Class

If you see an error message such as cannot resolve symbol 'ActivityMainBinding' then this means that the data binding auto-generated class has not been created. Check the following to resolve the issue:

  1. Make sure you have the proper dataBinding true in gradle and trigger "Sync with Gradle"
  2. Open the layout file and ensure that the XML file is valid and is wrapped in a <layout> tag.
  3. Check the layout file for the correct name i.e activity_main.xml maps to ActivityMainBinding.java.
  4. Run File => Invalidate Caches / Restart to clear the caches.
  5. Run Project => Clean and Project => Re-Build to regenerate the class file.
  6. Restart Android Studio again and then try the above steps again.

Data Binding does not exist messages

If you see an error message such as **.**.databinding does not exist, it's likely that there is an error in your data binding XML templates that needs to be resolved. Make sure to look for errors (i.e. forgetting to import a Java/Kotlin class when referencing it within your template).

Make sure that all your XML files with data binding have their errors fully resolved and the files are saved. Then be sure to Project -> Clean and Project -> Rebuild Project the project in order to regenerate the binding classes with latest properties.

Data Binding is not picking up certain layout fields

Be sure to Project -> Clean Project and Project -> Rebuild Project the project in order to regenerate the binding classes with latest layout properties.

Identifiers must have user defined types from the XML file. View is missing it

This usually means that you have a missing import statement or a mismatch between the type of variable in the XML and the actual object of the class passed in while binding. Check out these possible posts for solutions:

Please refer to these and carefully check for missing imports or type mismatches. Also try Build -> Clean -> Rebuild Project to recompile the binding classes as well to see if this helps.

Dagger 2 compatibility errors

If you are using the data binding library with Dagger 2, you may see errors such as NoSuchMethodError: ...FluentIterable.append(...). The solution to this fix is to add the Guava library before the Dagger compiler. There appears to be a known issue that can only be resolved by forcing the Dagger compiler to use a newer Guava version (Dagger 2 appears to have an older version of Guava bundled and without an explicit dependency it uses this version).

apt 'com.google.guava:guava:19.0' // add above dagger-compiler
apt 'com.google.dagger:dagger-compiler:2.5' 

References

Fork me on GitHub