
Custom attributes using BindingAdapters in Kotlin
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.
An example of these methods is available on this demo project.