Data Structures & Algorithms in Dart, First Edition!

Dive into stacks, queues, trees, graphs, efficient sorting and searching algorithms, and more—with Dart, a delightful programming language.

Home Android & Kotlin Tutorials

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.

Version

  • Kotlin 1.5, Android 4.4, Android Studio 2020.3.1

The Data Binding Library, part of Android Jetpack, provides an easy way to bind components in your layouts with data sources in your app. You can use a declarative format to make the binding instead of doing it programmatically.

In this tutorial, you’ll build an app named uSpace. This app shows four different lists with information about SpaceX rockets, crew, Dragons and capsules.

Over the course of the tutorial, you’ll learn about:

  • Data binding
  • Using data binding with RecyclerViews
  • Binding adapters
  • Custom binding adapters
  • Conversions
  • Two-way data binding
Note: This tutorial assumes you know the basics of Android development. If you’re new to Android development, check out Beginning Android Development and Kotlin for Android: An Introduction.

Getting Started

Download the materials by clicking the Download Materials button at the top or bottom of this tutorial. Open Android Studio Arctic Fox or later and import the starter project.

Below is a summary of what each package contains:

  • bindingadapters: All classes related to binding adapters.
  • di: Classes for providing dependency injection.
  • network: Classes related to the connection with the API.
  • repository: Repository related code.
  • ui: Classes related to the user interface.
  • utils: Utility methods used in the app.

Open the module build.gradle file. Find the buildFeatures section and enable data binding, as shown below:

buildFeatures {
    viewBinding true
    dataBinding true
}

Build and run the app. You’ll see a screen with the action bar and some tabs as shown below:

uSpace with empty list

Note: If you see an error loading message, make sure your internet connection is turned on.

Notice that the items in the list aren’t showing correctly. Navigate to the other tabs and you’ll see the same behavior. You’ll fix that using data binding and binding adapters.

Introducing Data Binding

The interaction between code and layout used to happen in the activity or fragment using findViewById(). More recently, if you used View Binding, you needed to reference the UI component and use the component methods and listeners to interact between code and layout. Using View Binding, you ended up with a lot of boilerplate code to access and modify the UI components in your layouts.

But with the Data Binding Library from Android Jetpack, you can interact directly with the components in your layouts.

Note: For a more in-depth tutorial about the basics of data binding, check out the following tutorial: Data Binding in Android: Getting Started.

Understanding Data Binding

With data binding, you can use binding expressions to assign values and handle events dispatched by the views. A one-way binding expression has the following form: android:text="@{viewModel.value}". This expressions tells the layout that android:text will have the value viewModel.value. You can use an object, a view model or individual variables to bind the code logic with the layout.

Binding adapters let you bind your code with your layout. Basic binding adapters call setters in the view to set a value, while more advanced adapters add logic to execute while performing data binding. The following diagram shows binding adapters’ place in the data binding process:

Diagram explaining data binding

To use data binding in your apps, you need to:

  1. Enable data binding in your project. You already implemented this step at the beginning of this tutorial.
  2. Add a <layout> in your views.
  3. Add a ViewModel, variables or object you need to the layout.

It’s time to start using data binding in uSpace.

Initializing Data Binding

The Rockets tab needs to show a ProgressBar whenever the app is loading the data. Open fragment_rockets.xml and add a <layout> component as the parent element in the view. All existing elements in the layout need to be children of <layout>. The layout should look as follows:

<?xml version="1.0" encoding="utf-8"?>
<layout>
  <androidx.constraintlayout.widget.ConstraintLayout 
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ui.fragments.RocketsFragment">

    <androidx.recyclerview.widget.RecyclerView
      android:id="@+id/rockets_list"
      android:layout_width="match_parent"
      android:layout_height="match_parent"
      tools:listitem="@layout/item_rocket" />

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

  </androidx.constraintlayout.widget.ConstraintLayout>
</layout>

Add <data> as a child of <layout>. It’ll hold the variables you can use in the layout file. You can add one or multiple variables in the layout. In this case, you need to add a Boolean with the name loading, as follows:

<data>
  <variable
    name="loading"
    type="Boolean" />
</data>

variable has two attributes:

  1. name: You can access the provided object in this layout with this name.
  2. type: If you use a custom object or view model, you need to add its complete package name.

Later on, you’ll add a custom binding adapter to use this variable to display or hide the progress bar on this screen.

Whenever you want to use data binding in your layouts, perform these steps. However, it’s more complicated to use data binding in RecyclerViews so in following section, you’ll learn how to set up RecyclerView‘s adapters to use data binding.

Binding Data in RecyclerViews

To start using data binding in a RecyclerView, open item_capsule.xml and add <layout> as the root element in the layout. Then add <data> with a variable of type Capsule in it, as shown below:

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

Once you add the variable to the layout, you need to set its value in the adapter. Android Studio creates a BR class that includes the reference to the variable you added. Be sure to rebuild your project before continuing.

Note: You can find the BR class file in your app ▸ build ▸ generated ▸ source ▸ kapt▸debug(buildVariant) ▸ com.raywenderlich.android.uspace folder.

Open CapsuleAdapter.kt and modify bind() as follows:

fun bind(capsule: Capsule) {
  binding.setVariable(BR.capsule, capsule)
}

binding contains the reference to the list item’s layout. Using setVariable(), assign the value of the object it needs to the layout. BR.capsule is the name of the variable you’ll set and capsule is the value you’ll pass to the layout.

Next, import the following class:

import com.raywenderlich.android.uspace.BR

You’ve completed initializing data binding for the Capsules list. The time has come to start using binding adapters.

Understanding Binding Adapters

Binding adapters are responsible for binding the code to your layout elements. They’ll call the UI element’s setters to assign the value, execute some custom logic or listen to user interaction in the view. You’ll use these three types of binding adapters in the following sections.

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.

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.

Using a Binding Adapter

Open RadioGroupBindingAdapters.kt and add the following code:

@BindingAdapter("crewFilter")
fun RadioGroup.setCheckedButton(crewAgency: MutableLiveData<CrewAgency>?) {
  val selectedId = when (crewAgency?.value) {
    CrewAgency.SPACEX -> R.id.spacex
    CrewAgency.NASA -> R.id.nasa
    CrewAgency.JAXA -> R.id.jaxa
    CrewAgency.ESA -> R.id.esa
    else -> null
  }

  if (selectedId != null && selectedId != checkedRadioButtonId) {
    check(selectedId)
  }
}

You’re adding the annotation @BindingAdapter, which will respond to values set to crewFilter. Depending on CrewAgency, the adapter returns the ID that will select the correct radio button. To prevent infinite cycles, you need to add an if condition to check the radio button only if the selected ID has changed.

The next step is to return a CrewAgency depending on the selected radio button.

Creating an InverseBindingAdapter

To achieve this, you need to create an inverse binding adapter. In RadioGroupBindingAdapter.kt, add the following code:

@InverseBindingAdapter(attribute = "crewFilter")
fun RadioGroup.getCheckedButton(): CrewAgency? {
  return when (checkedRadioButtonId) {
    R.id.spacex -> CrewAgency.SPACEX
    R.id.nasa -> CrewAgency.NASA
    R.id.jaxa -> CrewAgency.JAXA
    R.id.esa -> CrewAgency.ESA
    else -> null
  }
}

Here, you add @InverseBindingAdapter to a method and set the attribute name to which this inverse binding adapter will respond. It should always be the same attribute name as the one used in the normal binding adapter. In this case, the attribute name is crewFilter.

Finally, the binding adapters need a way to know when the attributes change.

Binding Listener Methods

To give the binding adapters a way to listen for changes in their attributes, add the following code to RadioGroupBindingAdapters.kt:

@BindingAdapter("app:crewFilterAttrChanged")
fun RadioGroup.setListeners(listener: InverseBindingListener?) {
  listener?.let {
    setOnCheckedChangeListener { radioGroup, id ->
      listener.onChange()
    }
  }
}

This is a binding adapter for the attribute app:crewFilterAttrChanged. The name of this attribute is the name you give to your attribute plus AttrChanged. This adapter receives InverseBindingListener. This listener has one method: onChange(). In this case, you’ll call onChange() whenever the checked state of the radio button changes.

Finally, it’s time to use two-way data binding. Open fragment_crew.xml and modify RadioGroup as follows:

<RadioGroup
  android:id="@+id/filter"
  android:layout_width="match_parent"
  android:layout_height="wrap_content"
  android:orientation="horizontal"
  app:crewFilter="@={viewModel.crewAgency}"
  app:layout_constraintEnd_toEndOf="parent"
  app:layout_constraintStart_toStartOf="parent"
  app:layout_constraintTop_toTopOf="parent">

This code adds crewFilter to RadioGroup. Two-way data binding uses a different notation: @=. This notation adds two-way data binding using crewAgency.

Finally, open CrewFragment.kt and modify onViewCreated() as follows:

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

  binding?.viewModel = viewModel

  setupList()

  viewModel.result.observe(viewLifecycleOwner) { result ->
    handleResult(result)
  }

  viewModel.crewAgency.observe(viewLifecycleOwner) {
    adapter.addItems(viewModel.getFilteredCrew())
  }
  viewModel.getCrew()
}

Here, you assign the instance of viewModel to the binding, so the layout has access to it.

Note: You might see an error in binding?.viewModel. Remember to build the project after adding <data> to the layout so that Android Studio creates the necessary files for you.

Build and run. Open the Crew tab. Select any radio button to filter the crew by agency. You’ll see something similar to the image below:

Crew tab with filter

Great job! You’ve implemented two-way data binding.

Where to Go From Here?

Download the final project by using the Download Materials button at the top or bottom of the tutorial.

To learn more about data binding, visit the official documentation. You can learn more about view binding, which is related to data binding, in the View Binding Tutorial for Android: Getting Started.

I hope you enjoyed this tutorial on data binding. If you have any questions or comments, please join the forum discussion below.

Reviews

More like this

Contributors

Comments