Android Data Binding: Custom Setters

Make Data Binding Do What You Want

George Mount
Published in
5 min readAug 26, 2016

--

I hope that those of you who have read my previous articles on Android Data Binding have been playing with it. You’ve probably been binding android:text to TextViews, android:checked to CheckBoxes, and maybe even tried two-way data binding with android:text on EditText. The built-in attributes are pretty well covered and new tags were synthesized in order to bind to events. However, that doesn’t help with custom Views. Also, how can you customize the setting logic?

Binding To Setters

My app has a custom View, ColorPicker:

public class ColorPicker extends View {
private int color;

public void setColor(int color) {
this.color = color;
invalidate();
}

public int getColor() {
return color;
}
public void addListener(OnColorChangeListener listener) {
//...
}
public void removeListener(OnColorChangeListener listener) {
//...
}
//...
}

I would love to set the color with data binding. It turns out that this type of setter does not require any code. The data binding system automatically looks for a setter with the same name as the data bound attribute.

<com.example.myapp.ColorPicker
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:color="@{color}"
/>

Binding Adapters

Sometimes we want to do something more complex than simply calling a setter on the View. My favorite example is loading images off the UI thread. First, I need to choose a custom attribute to assign the image to an ImageView.

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{product.imageUrl}"
/>

If I didn’t do anything, the data binding system would look for a “setImageUrl(String)” on ImageView and not find it. I have to create a way to set the “app:imageUrl” attribute. Binding adapters are annotated methods in any class that are used to do just this. Typically, you’d organize your adapters into a classes based on the target View type.

I’m not going to promote any specific image library, so let’s assume a generic one for our binding adapter:

@BindingAdapter("imageUrl")
public static void setImageUrl(ImageView imageView, String url) {
if (url == null) {
imageView.setImageDrawable(null);
} else {
MyImageLoader.loadInto(imageView, url);
}
}

The BindingAdapter annotation takes the attribute name as its parameter. Anything in the application namespace doesn’t need any namespace in the parameter, but for attributes in the android namespace, you must give the full attribute name including the “android.”

The first method parameter is the type of the target View. The second is the value being set to the attribute.

The MyImageLoader library will load the image off the UI thread and set it into the ImageView when it completes. While the UI thread no longer hangs while the image loads, the ImageView remains empty during the load and I really want a placeholder there.

<ImageView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:imageUrl="@{product.imageUrl}"
app:placeholder="@{@drawable/shadowAvatar}"
/>

Now, the two attributes interact. If the product image has already been loaded, I don’t want the placeholder image to show. When the image does load, I want the loader to be able to cross-fade the images. Therefore, our binding adapter must handle multiple attributes:

@BindingAdapter(value={"imageUrl", "placeholder"}, requireAll=false)
public static void setImageUrl(ImageView imageView, String url,
Drawable placeHolder) {
if (url == null) {
imageView.setImageDrawable(placeholder);
} else {
MyImageLoader.loadInto(imageView, url, placeholder);
}
}

When handling multiple attributes, the attributes must be placed inside a list. The parameters of the method are in the same order as in the value. I’ve also set the requireAll to false. If I left it to its default value of true, the binding adapter would require both the URL and placeholder to be called. With requireAll set to false, if either the URL or placeholder value is not set, the binding adapter can still be called with default values (null in this case) for the unset attributes.

Within this binding adapter, MyImageLoader can do all the magic it needs, operating on the ImageView with both the URL and placeholder image.

Setting Event Handlers

When setting event handlers, simple setters are pretty easy — you just use the same name for the attribute as the setter method. Often you want to be able add arbitrary numbers of listeners. In that case, instead of a “set” method, you have “add” and “remove” methods.

My ColorPicker has just that kind of listener for the “onColorChange” event. The easiest thing to do is to remove the old listener and then add the new one. Fortunately, data binding gives us that exact functionality. To retrieve the old value, the binding adapter method should take two parameters for each attribute, with all old values coming first, then all new values:

@BindingAdapter("onColorChange")
public static void setColorChangeListener(ColorPicker view,
OnColorChangeListener oldListener,
OnColorChangeListener newListener) {
if (oldListener != null) {
view.removeListener(oldListener);
}
if (newListener != null) {
view.addListener(newListener);
}
}

You see how useful the old value is for this binding adapter. Old values aren’t only for event listeners, it is just the most common usage. Any time you can’t retrieve the old value from the View and need it, just add parameters for the old value.

Our listener is declared like this:

public interface OnColorChangeListener {
void onColorChange(ColorPicker colorPicker, int newColor);
}

Because OnColorChangeListener has exactly one abstract method, the data binding framework can figure out what to do with lambda methods and method references:

<com.example.myapp.ColorPicker
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:onColorChange="@{(v, color)->handler.colorChanged(color)}"
app:color="@{color}"
/>

It automatically associates the abstract method (in this case onColorChange) with the declared lambda expression. This is all done at compile time, so you don’t have any reflection in your application.

Name your event attributes the same as the method name to prevent your developers from being confused.

Conclusion

Android Data Binding does what you want most of the time without any code — just call the setter on your View. When you want something more complicated, you also have the ability to tweak it to your needs with binding adapters.

People have come up with some great and interesting uses for binding adapters. For example, Lisa Wray used a binding adapter to set a custom font. You can also animate values from one state to the next. Maybe you can come up with some great uses. If you think of something clever, please post it and let us know.

--

--