Advanced Data Binding in Android: Binding Adapters

In this advanced data binding tutorial, you’ll learn how you can interact directly with the components in your layouts, assign a value and handle events dispatched by the views using binding adapters. By Rodrigo Guerrero.

Leave a rating/review
Download materials
Save for later
Share
You are currently viewing page 3 of 4 of this article. Click here to view the first page.

Using Multiple Attributes

Open ImageBindingAdapters.kt and modify the binding adapter like this:

@BindingAdapter("imageUrl", "placeholder")
fun ImageView.loadImage(url: String, placeholder: Drawable) {
  Picasso.get().load(url).placeholder(placeholder).into(this)
}

This binding adapter receives two attributes: one with the image URL and the other with Drawable that will show as a placeholder while the image loads.

Open item_rocket.xml and update it as follows:

<ImageView
  android:id="@+id/rocket_image"
  android:layout_width="@dimen/item_image_size"
  android:layout_height="@dimen/item_image_size"
  android:scaleType="centerCrop"
  app:imageUrl="@{rocket.images[0]}"
  app:placeholder="@{@drawable/splash_background}"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent" />

With this, you add the placeholder attribute to ImageView. Build and run. You’ll see the placeholder while the image is loading.

Use this same binding adapter in the other screens too. Open item_crew.xml and add imageUrl and placeholder to ImageView as follows:

<ImageView
  android:id="@+id/crew_image"
  android:layout_width="@dimen/item_image_size"
  android:layout_height="@dimen/item_image_size"
  android:scaleType="centerCrop"
  app:imageUrl="@{crew.image}"
  app:placeholder="@{@drawable/splash_background}"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent" />

Finally, open item_dragon.xml and modify its ImageView as follows:

<ImageView
  android:id="@+id/dragon_image"
  android:layout_width="@dimen/item_image_size"
  android:layout_height="@dimen/item_image_size"
  android:scaleType="centerCrop"
  app:imageUrl="@{dragon.images[0]}"
  app:placeholder="@{@drawable/splash_background}"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent" />

Build and run. Go to the Crew tab and you’ll see the placeholder and the crew images, like in the image below:

Crew tab with images

Open the Dragons tab. You’ll now see the images there too.

Dragons tab with images

Next, you’ll create other custom binding adapters to show more information in the lists.

Adding Other Custom Binding Adapters

For the rockets list, you need to show the rocket weight and height. Rocket contains a Weight that has the weight value in its kg. The class also has Measurement that has the height value in its meters. To add the units, use R.string.rocket_weight_kg and R.string.rocket_height_m.

Open TextViewBindingAdapters.kt and add the following code:

@BindingAdapter("rocketWeight")
fun TextView.addRocketWeight(weight: Weight) {
  val formattedWeight = NumberFormat.getInstance().format(weight.kg)
  text = context.getString(R.string.rocket_weight_kg, formattedWeight)
}

@BindingAdapter("rocketHeight")
fun TextView.addRocketHeight(height: Measurement) {
  text = context.getString(R.string.rocket_height_m, height.meters)
}

This code creates two binding adapters that use rocketWeight and rocketHeight. For the rocket weight, you use NumberFormat to separate the number with commas. You show the two values with the string resources.

Open item_rocket.xml and modify rocket_height and rocket_weight TextViews to use these binding adapters, like this:

<TextView
  android:id="@+id/rocket_height"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  app:rocketHeight="@{rocket.height}"
  app:layout_constraintStart_toEndOf="@id/rocket_image"
  app:layout_constraintTop_toBottomOf="@id/rocket_name"
  tools:text="22.25 meters" />

<TextView
  android:id="@+id/rocket_weight"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  app:rocketWeight="@{rocket.mass}"
  app:layout_constraintStart_toEndOf="@id/rocket_image"
  app:layout_constraintTop_toBottomOf="@id/rocket_height"
  tools:text="30,000 kg" />

Here, you’re using app:rocketHeight to display the height and app:rocketWeight to display the weight. Build and run. You’ll see the rocket items with all the information, like this:

Rockets tab with complete data

Next, you’ll count items in a list and show the result in the layout. Open TextViewBindingAdapters.kt and add the following code:

@BindingAdapter("launches")
fun TextView.numberOfLaunches(crew: Crew) {
  val numberOfLaunches = crew.launches.count()
  text = context.getString(R.string.launches, numberOfLaunches)
}

This code counts the number of launches for each crew member and set a string with this value. Open item_crew.xml and add app:launches in it, like this:

<TextView
  android:id="@+id/crew_launches"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
  app:launches="@{crew}"
  app:layout_constraintStart_toEndOf="@id/crew_image"
  app:layout_constraintTop_toBottomOf="@id/crew_agency" />

Finally, you need to show a formatted date in the Dragon items, documenting the first launch date. You also need to capitalize the first letter in the Dragon type. Open TextViewBindingAdapter.kt and add the following code:

@BindingAdapter("date")
fun TextView.formatDate(date: String) {
  val formatter = SimpleDateFormat("yyyy-MM-dd", Locale.getDefault())
  formatter.parse(date)?.also {
    val finalFormatter = SimpleDateFormat("MMMM dd, yyyy", Locale.getDefault())
    text = finalFormatter.format(it)
  }
}

This binding adapter formats the provided date and displays it in TextView. Now, add the following code to capitalize the first letter of Dragon:

@BindingAdapter("capitalizeFirst")
fun TextView.capitalizeFirst(value: String) {
  text = value.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
}

This binding adapter capitalizes the first letter of the provided string.

Open item_dragon.xml and modify dragon_date and dragon_type to use app:date and app:capitalizeFirst, as follows:

<TextView
  android:id="@+id/dragon_date"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
  app:date="@{dragon.firstFlightDate}"
  app:layout_constraintStart_toEndOf="@id/dragon_image"
  app:layout_constraintTop_toBottomOf="@id/dragon_name" />

<TextView
  android:id="@+id/dragon_type"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle2"
  app:capitalizeFirst="@{dragon.type}"
  app:layout_constraintStart_toEndOf="@id/dragon_image"
  app:layout_constraintTop_toBottomOf="@id/dragon_date" />

Build and run. Go to the Crew tab and you’ll see the number of launches:

Crew tab with number of launches

Finally, open the Dragons tab and you’ll see the Dragon first launch date and its type, as shown in the next image:

Dragons tab with launch date

Sometimes, you don’t need to add any extra logic to the binding and only convert from one type of object to another. You’ll learn this next.

Learning About Conversions

If you look closely, the crew members’ names are missing. You’re going to convert Crew to a string to display names.

Open Converters.kt and add the following code:

@BindingConversion
fun crewToName(crew: Crew): String = crew.name

A conversion is a method that receives an object from one type and returns another type. Add @BindingConversion to indicate that this method is a conversion.

Open item_crew.xml and modify crew_name TextView to set the text using Crew, as shown below:

<TextView
  android:id="@+id/crew_name"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:text="@{crew}"
  android:textAppearance="@style/TextAppearance.MaterialComponents.Headline6"
  app:layout_constraintStart_toEndOf="@id/crew_image"
  app:layout_constraintTop_toTopOf="parent" />

Data binding looks for a method that receives an object of type Crew and returns a String. Before you added the binding conversion method above, such a method didn’t exist.

Build and run. Open the Crew tab and you’ll see each crew member with their name, as shown in the following image:

Crew tab with names

Another conversion that can come in handy is to set a text appearance using predefined strings. Open Converters.kt and add the following code:

@BindingConversion
fun convertStringToTextAppearance(style: String): Int {
  return when (style) {
    "title" -> R.style.TextAppearance_MaterialComponents_Headline6
    "height" -> R.style.TextAppearance_MaterialComponents_Subtitle1
    "weight" -> R.style.TextAppearance_MaterialComponents_Subtitle2
    else -> R.style.TextAppearance_AppCompat_Body1
  }
}

This conversion takes a style in a string form and returns the corresponding style for the text appearance.

Open item_rocket.xml and modify TextViews to set the right text appearances, like this:

<TextView
  android:id="@+id/rocket_name"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:text="@{rocket.name}"
  android:textAppearance="@{@string/title_appearance}"
  app:layout_constraintStart_toEndOf="@id/rocket_image"
  app:layout_constraintTop_toTopOf="parent" />

<TextView
  android:id="@+id/rocket_height"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  android:textAppearance="@{@string/height_appearance}"
  app:rocketHeight="@{rocket.height}"
  app:layout_constraintStart_toEndOf="@id/rocket_image"
  app:layout_constraintTop_toBottomOf="@id/rocket_name" />

<TextView
  android:id="@+id/rocket_weight"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  android:textAppearance="@{@string/weight_appearance}"
  app:rocketWeight="@{rocket.mass}"
  app:layout_constraintStart_toEndOf="@id/rocket_image"
  app:layout_constraintTop_toBottomOf="@id/rocket_height" />

Since textAppearance receives a style resource, it wouldn’t know how to set a string without the conversion you just created. Now, textAppearance will use the code above to convert the string to a style resource.

Build and run. You’ll see that the rocket name, height, and weight now have an improved appearance:

Rockets tab with styles

You’ve learned how to set values to the views. However, sometimes you’ll also need to receive values or events from views — two-way data binding to the rescue.

Using Two-Way Data Binding

So far, you’ve been using one-way data bindings. One-way bindings set a value to the view and listen to the value changes, as shown in the diagram below.

One-way binding

With two-way data binding, you set a value to the view and listen to the value changes at the same time:

Two-way data binding

You’ll use two-way data binding to implement a filter. This filter will show crew members filtered by which space agency they belong to. First, create a binding adapter to set the value to the view.