Custom attributes using BindingAdapters in Kotlin

Manpreet Mall
4 min readDec 18, 2020

Using the Android data binding framework it’s easy to define a custom attribute that can be used in a layout files. It’s just a static method with the right parameters annotated with @BindingAdapter. The most common example is a method that allows to use Glide or Picasso to populate an ImageView downloading the image from an url:

@BindingAdapter("bind:imageUrl")
public static void setImageUrl(ImageView view, String url) {
Glide.with(view.getContext()).load(url).into(view);
}

In a layout the imageUrl attribute can be defined using the app namespace:

<ImageView
android:id="@+id/avatar"
android:layout_width="@dimen/photo_size"
android:layout_height="@dimen/photo_size"
android:scaleType="centerCrop"
app:imageUrl="@{user.avatarUrl}"/>

This example is written in Java, so the method must be defined as static. What about Kotlin? In Kotlin the static keyword doesn’t exist but this language is less strict than Java about method definition: a method can be defined outside a class.

The previous method can be translated into Kotlin using a file with just the method (it doesn’t contain any classes):

@BindingAdapter("imageUrl")
fun setImageUrl(imageView: ImageView, url: String?) {
Glide.with(imageView.context).load(url).into(imageView)
}

The usage in the layout is the same, we can still define the app:imageUrl attribute using the same syntax.

This method can be used also in a normal Kotlin class that doesn’t use the data binding framework, it can be invoked passing an ImageView and an String as arguments:

setImageUrl(myImageView, "myUrl")

Extension functions

In Kotlin we can simplify this line of code defining the method as extension function on the ImageView class. Let’s refactor the setImageUrl method:

@BindingAdapter("imageUrl")
fun ImageView.setImageUrl(url: String?) {
Glide.with(context).load(url).into(this)
}

It can seem strange but this is still a valid BindingAdapter method, we can still use the imageUrl attribute in a layout file. The reason is simple: the three versions of this method (the static Java method, the Kotlin method with an ImageView parameter and the extension method) are translated into the same bytecode. More info about this subject are available in this post written by Lorenzo Quiroli.

The advantage of the extension method is that now we can use it directly on an ImageView object:

myImageView.setImageUrl("myUrl")

Properties

The first two BindingAdapter I always define in an Android project are these two:

@BindingAdapter("visibleOrGone")
fun View.setVisibleOrGone(show: Boolean) {
visibility = if (show) VISIBLE else GONE
}
@BindingAdapter("visible")
fun View.setVisible(show: Boolean) {
visibility = if (show) VISIBLE else INVISIBLE
}

Using these two adapters the visibility of a View can be bound to a boolean instead of an Int with the three states VISIBLE, INVISIBLE and GONE. In a layout the attribute can be defined in the usual way using the app namespace:

app:visibleOrGone="@{state.myBoolean}"

In Kotlin code we can use these methods to modify the visibility of a View. Another extension method that returns a Boolean with the visibility could be useful too, it can be written in a single line of code:

fun View.isVisible() = visibility == VISIBLE

Ok, wait a moment… We are using Kotlin so we don’t need to write getters and setters, we can use the properties! But can we define an extension property on the View class and use the setter as BindingAdapter method? Yes, it’s possible using the prefix set on the BindingAdapter annotation:

@set:BindingAdapter("visibleOrGone")
var View.visibleOrGone
get() = visibility == VISIBLE
set(value) {
visibility = if (value) VISIBLE else GONE
}
@set:BindingAdapter("visible")
var View.visible
get() = visibility == VISIBLE
set(value) {
visibility = if (value) VISIBLE else INVISIBLE
}

The usage in a layout doesn’t change, we can still use app:visibleOrGone and app:visible attributes. But now the property can be easily used in Kotlin code to retrieve and change the visibility:

if (!myView.visibleOrGone) {
myOtherView.visible = true
}

We can simplify the code further defining other two properties invisible and gone:

@set:BindingAdapter("invisible")
var View.invisible
get() = visibility == INVISIBLE
set(value) {
visibility = if (value) INVISIBLE else VISIBLE
}
@set:BindingAdapter("gone")
var View.gone
get() = visibility == GONE
set(value) {
visibility = if (value) GONE else VISIBLE
}

Using these properties we can avoid the negation in Kotlin file and in data binding expressions, the previous code can be rewritten:

if (myView.gone) {
myOtherView.visible = true
}

Wrapping up

Custom expressions in data binding are very powerful and can be used to simplify layouts and to avoid complex binding expressions. Using Kotlin they can be defined in many ways, you can choose the best way based on the usage you want to achieve. For write only expression a top level extension method is probably the best solution, but if the attribute can be read from Kotlin code a property can be really useful.

--

--