19 July 2016

Using resource IDs in data binding layouts

I have been playing with data binding more and more over the last couple of weeks. This week, it's all about creating a dialog with stuff dictated by a value from an enum.

Each value in this enum has the following properties:
@IdRes int textView
@StringRes int quote
@StringRes int name
@ColorRes int colour

I have added a new button in my Sandbox app to choose one value from the enum and display the properties in an alert dialog via data binding. Ambitious, I know!

The relevant Java part is mainly just creating and showing the dialog:
@OnClick(R.id.data_binding_alert)
public void onSendDataBoundAlert() {
   AlertDialog.Builder builder = new AlertDialog.Builder(MainActivity.this, R.style.MaterialAlertDialog);
   builder.setPositiveButton(android.R.string.ok, null)
           .setNegativeButton("Not now", null);
   View view = LayoutInflater.from(MainActivity.this).inflate(R.layout.dialog_data_binding_demo, null);
   DialogDataBindingDemoBinding binding = DataBindingUtil.bind(view);
   binding.setCharacter(getRandomCharacter());
   builder.setView(view);

   builder.create().show();
}

The tricky part is telling data binding how to use our resource IDs. Say I want the colour to be set as a background for a TextView, and the name to be displayed:
<TextView
     android:id="@+id/name"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:minHeight="64dp"
     android:textSize="24sp"
     android:textStyle="bold"
     android:gravity="center_vertical"
     android:paddingLeft="16dp"
     android:background="@{character.colour}"
     android:text="@{character.name}"/>

Running the sample though, you'll see that the colour is not set and a dark grey-ish lilac-ish colour is used instead:


Unacceptable indeed!

So how do we make the Earl of Lemongrab happy? There are two ways of solving this problem.

A. Use a BindingAdapter

A BindingAdapter lets you manipulate and do a bit more involved logic on your data before applying it to the View. To use a BindingAdapter, first create a static method in your code that is bound to either a standard Android attribute or a custom one.

I create a custom attribute here called characterBackground:
@BindingAdapter({"characterBackground"})
public static void characterBackground(TextView textView, AdventureTimeCharacters character) {
     textView.setBackgroundColor(ContextCompat.getColor(textView.getContext(), character.getColour()));
}

You can then use this BindingAdapter in the TextView:
app:characterBackground="@{character}"
Do not forget to add the app namespace! Android Studio can add this for you. Just type in appNs and it will autocomplete.

This solution works, but is a bit too involved. And you said data binding is easy!!!

Well it kinda is, because, it turns out that we can:
B. Set the value in XML

But it didn't work! Well it turns out the problem is because we are passing a resource ID into an attribute that expects a resolved color value. So the only thing we need to do is to move all that code in our BindingAdapter into XML.

First add the import:
<data>
    . . .
    <import type="android.support.v4.content.ContextCompat" />
</data>

And then use it:
<TextView
     android:id="@+id/name"
     android:layout_width="match_parent"
     android:layout_height="wrap_content"
     android:minHeight="64dp"
     android:textSize="24sp"
     android:textStyle="bold"
     android:gravity="center_vertical"
     android:paddingLeft="16dp"
     android:background="@{ContextCompat.getColor(context, character.colour)}"
     android:text="@{character.name}"/> 

Very convenient! And it works! Note that you do not have to import anything to use context, there is some data binding magic going on that automatically injects it for you if you call it. Think of it as The Beckoning of the Context.


The Earl is now happy.

PS: If you need to set a drawable from a resource ID as an ImageView src, I found that you would also need to do this trick, aka:
android:src="@{ContextCompat.getDrawable(context, character.icon)}"


15 July 2016

Quick tip: Enabling data binding

Say you have a simple layout file:
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">

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

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

</LinearLayout>
And say you want to finally give data binding a try. So you watch Yigit's I/O talk on data binding and follow the steps on implementation.

You enable it in Gradle:
android {
    dataBinding {
        enabled = true
    }
}

You wrap your existing layout in <layout> tags.
<layout>
      <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">

        ...

      </LinearLayout>
<layout>

Then life started sucking. Your app won't compile. Well, we can fix it in two easy steps!

Step 1: Add namespace declaration to your root tag:
<layout xmlns:android="http://schemas.android.com/apk/res/android">

This still won't compile! If you look at the logs you will see somewhere there: "Error:(7) Error parsing XML: duplicate attribute"

Step 2: Remove xmlns declaration in the now-non-root layout.

And that's it. Add your variables and happy data binding!


Here's a complete layout file for your perusal:
<layout xmlns:android="http://schemas.android.com/apk/res/android">
      <data>
        <variable
            name="listing"
            type="com.zdominguez.blog.samples.Listing"/>
    </data>

      <LinearLayout 
                  android:layout_width="match_parent"
                  android:layout_height="match_parent"
                  android:orientation="vertical">

        <TextView
            android:id="@+id/title"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{listing.address}" />

        <TextView
            android:id="@+id/description"
            android:layout_width="match_parent"
            android:layout_height="wrap_content" 
            android:text="@{listing.agency}" />

      </LinearLayout>
<layout>