BindingAdapter - a simple solution to the View layer’s problems in your Android apps

Short and clear code makes every application easy to maintain and understand. Programmers should use the most effective solutions to save their time. Below you can find a few advices - I think it’s worth to follow them and start using Data Binding Library - as I will try to prove in this article it offers really multiple solutions.

I love writing specific parts of code in their right place – I always set up as many attributes of a View class in XML files as possible. In the example below I prepared my custom view.

public class MyView extends View {  
    private String mImage;

    // ...

    public void setImage(@Nullable String image) {
        mImage = image;
        invalidate();
    }

    public @Nullable String getImage() {
        return mImage;
    }
}

Standard setter method doesn’t require any special code to use it. Data Binding automatically finds a setter that has the same name as the expected attribute:

<com.example.views.MyView  
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:image="@{imageUrl}"/>

This is a really simple example: usually, we want to do more complex operations. Data Binding library contains a special solution for it – BindingAdapter. It allows us to manage how values should be set to views with expressions.

@BindingAdapter("visibleIf")
public static void changeVisibility(@NonNull View view, boolean visible) {  
    view.setVisibility(visible ? View.*VISIBLE *: View.*GONE*);
}

The attribute name of BindingAdapter annotation is its parameter. The first parameter is the type of the used View. The other one is the value that will be set as the attribute. In the above example, the method changes view’s visibility according to the value of "visible” parameter.

<com.example.views.MyView  
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:visibleIf="@{isVisible}"/>

BindingAdapter supports setting multiple attributes that must be placed as a list. In this case, the parameters of the method are in the same order.

@BindingAdapter(value = {"inputText", "errorText"})
public static void setInputWithError(@NonNull EditText editText, @NonNull String input, @NonNull String error) {  
    editText.setError(TextUtils.*isEmpty*(input) ? error : null);
}
<EditText  
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:errorText="@{@string/emptyText}"
    app:inputText="@{text}"/>

BindingAdapter requires all the parameters by default, but also allows us to change them. After adding "requireAll=false" a method will be called with default missed values. In the example below, it will be null.

@BindingAdapter(value = {"imageUrl", "errorDrawable"}, requireAll = false)
public static void loadImageWithError(@NonNull ImageView imageView,  
                                      @Nullable String url, @Nullable Drawable error) {
    if (url != null) {
        ImageLoader.load(imageView, url);
    } else if (error != null) {
        imageView.setImageDrawable(error);
    }
}
<ImageView  
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:errorDrawable="@{@drawable/errorPlaceholder}"/>

There is no fixed place in the code where BindingAdapter should be implemented - they can be defined anywhere, in the class of your choice. The most typical case would be to create a class named BindingAdapters and move it all there.

Actually, it’s only the developer’s imagination that limits the powers of BindingAdapter. It will be one of the most useful tools in every application you develop. Have a look at several examples of interesting use cases:

  • Setting a custom font:
@BindingAdapter("font")
public static void setCustomFont(@NonNull TextView textView, @NonNull String font) {  
    textView.setTypeface(TypefaceUtils.load(font));
}
  • Applying animation:
@BindingAdapter(value = {"scaleValue", "alphaValue", "interpolatorValue"})
public static void setCustomFont(@NonNull View view, float scale, float alpha, @NonNull Interpolator interpolator) {  
    view.animate()
            .scaleX(scale)
            .scaleY(scale)
            .alpha(alpha)
            .setInterpolator(interpolator)
            .start();
}
  • Loading image with specific transformation using Glide library:
@BindingAdapter(value = {"circleImageUrl", "placeholder", "error"})
public static void loadCircleImage(@NonNull ImageView imageView, @NonNull String url,  
                                   @NonNull Drawable placeholder, @NonNull Drawable error) {
    Glide.*with*(imageView.getContext())
            .load(url)
            .asBitmap()
            .placeholder(placeholder)
            .error(error)
            .transform(new CropCircleTransformation(imageView.getContext()))
            .into(imageView);
}

Conclusion

Data Binding is a powerful tool that will save you plenty of time – in many cases calling setter on your View will do the job for you. BindingAdapter gives you full control to create as complex methods as you want – "sky’s the limit".