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 2 of 4 of this article. Click here to view the first page.

Setting Values Automatically

The Data Binding Library provides ready-to-use binding adapters for some views. Let’s try this type of binding adapter in the Capsules tab.

To begin, add the capsule’s serial and type. Open item_capsule.xml. Modify capsule_name TextView to add the text attribute, as follows:

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

To set the value, add the binding expression @{capsule.serial}. Since the serial is of type String, the data binding library is going to look for setText(text: String) in TextView. This method already exists, so there’s nothing else you have to do to make this binding adapter work.

Note: You can add any code you want in a binding expression. However, it’s better practice to add complex logic in ViewModel and call its method instead.

Now, modify capsule_type TextView and add a line to bind it with the capsule type value, as follows:

<TextView
  android:id="@+id/capsule_type"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:layout_marginStart="@dimen/text_margin"
  android:paddingTop="@dimen/text_padding"
  android:text="@{capsule.type}"
  android:textAppearance="@style/TextAppearance.MaterialComponents.Subtitle1"
  app:layout_constraintStart_toEndOf="@id/capsule_image"
  app:layout_constraintTop_toBottomOf="@id/capsule_name" />

Same as before, you added the binding expression @{capsule.type} to text.

Build and run. Select the Capsules tab. Now you see the capsule name and type, like in the image below.

Capsules list with data

The Capsule list item is ready. There are other values you can set automatically. One of them is the rocket name in the Rockets tab.

Open item_rocket.xml, add <layout> as the parent element and add the following <data>:

<data>
  <variable
    name="rocket"
    type="com.raywenderlich.android.uspace.ui.models.Rocket" />
</data>

This variable is called rocket and its type is Rocket. Remember, for the type you need to add the complete package name where the class lives.

Once you’ve added rocket, you can use it. Set the rocket name, as follows:

<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}"
  app:layout_constraintStart_toEndOf="@id/rocket_image"
  app:layout_constraintTop_toTopOf="parent" />

The last step to make data binding work in RecyclerView is to open RocketsAdapter.kt and update bind() like this:

fun bind(rocket: Rocket) {
  binding.setVariable(BR.rocket, rocket)
}

Rebuild the project and import the BR class like you did earlier. This will set the rocket variable used in the layout.

Note: Other layouts already have <layout> and <data> set. bind() sets the needed variable for adapters.

The rocket name is the only value you’re allowed to assign in this item layout. Later, you’ll use custom binding adapters to set the remaining values.

Another value you can assign is name in the Dragons tab. Open item_dragon.xml and modify dragon_name TextView like this:

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

Finally, set the agency name in the Crew item. Open item_crew.xml and modify crew_agency TextView to display the agency name, like this:

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

In all these TextViews, you’re using automatic binding adapters, since the method to set the text with a string value already exists. Build and run. Open the Rockets tab and you can see the rocket name:

Rockets tab with rocket name

Open the Crew tab and you’ll see the agency name:

Crew tab with agency name

Go to the Dragons tab and you can see the Dragon names:

The remaining attributes need to execute some logic before they can have their values set. To do this, you have to create binding adapters.

Creating Custom Binding Adapters

With custom binding adapters, you’re implementing some logic that executes before binding data. You use these kinds of adapters when there’s no default adapter in the Data Binding Library.

In the following sections, you’ll create several custom binding adapters with logic that handles view visibility, load images and format strings.

Handling View Visibility

You may have noticed that there’s a progress bar in the Rockets tab that never goes away. You’ll fix that using a custom binding adapter to handle the view visibility.

Open ViewBindingAdapter.kt and add the following code:

@BindingAdapter("android:visibility")
fun View.setVisibility(visible: Boolean) {
  visibility = if (visible) {
    View.VISIBLE
  } else {
    View.GONE
  }
}

To create a custom binding adapter, you need to create an extension function of the view that will use the adapter. Then, you add the @BindingAdapter annotation. You have to indicate the name of the view attribute that will execute this adapter as a parameter in the annotation.

Note: In this example, you’re working with the attribute android:visibility. This attribute already exists in View class. However, you can create your own attributes whenever you need to.

In this case, every time android:visibility receives a Boolean, it’ll execute our setVisibility extension function.

Now, set loading to visibility. Open fragment_rockets.xml and modify ProgressBar as shown below:

<ProgressBar
  android:id="@+id/progress"
  android:layout_width="wrap_content"
  android:layout_height="wrap_content"
  android:visibility="@{loading}"
  app:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent" />

Using android:visibility="@{loading}" sets the view visibility using the binding adapter.

For ProgressBar to change its visibility, you need to set loading whenever the progress bar changes. Open RocketsFragment.kt and modify onViewCreated() as follows:

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  super.onViewCreated(view, savedInstanceState)

  setupList()

  viewModel.rockets.observe(viewLifecycleOwner) { result ->
    binding?.loading = false
    handleResult(result)
  }
  binding?.loading = true
  viewModel.getRockets()
}

binding references the loading you created in the layout. Using this reference, we set loading whenever it’s needed.

Build and run. Now, you see the ProgressBar while the data is loading, and it disappears when the app completes loading the data.

Custom binding adapters are also useful for loading images. You’ll learn this next.

Loading Images

Open ImageBindingAdapters.kt and create the following binding adapter:

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

Since you’ll use this adapter in ImageView, this adapter needs to be an extension function of that view. The attribute that the view will use is imageUrl. This is an example of a custom attribute. This binding adapter will use Picasso to load the image.

Next, modify ImageView in item_rocket.xml, 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:layout_constraintBottom_toBottomOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent" />

Here, you use app:imageUrl="@rocket.images[0]" to call the binding adapter. This is the custom attribute you created previously and the value in rocket, which has the image URL.

Build and run. You’ll see the rocket images loading, as in the next image:

Rockets tab with images

This binding adapter uses only one attribute, but you can add more. Let’s extend this binding adapter to show a placeholder while the image loads.