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
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:
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.
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:
To use data binding in your apps, you need to:
- Enable data binding in your project. You already implemented this step at the beginning of this tutorial.
- Add a
<layout>
in your views. - 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 variable
s 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:
-
name
: You can access the provided object in this layout with this name. -
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 RecyclerView
s 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.
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.
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.
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.
<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 TextView
s, 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:
Open the Crew tab and you’ll see the 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.
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:
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:
Open the Dragons tab. You’ll now see the images there too.
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:
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:
Finally, open the Dragons tab and you’ll see the Dragon first launch date and its type, as shown in the next image:
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:
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 TextView
s 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:
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.
With two-way data binding, you set a value to the view and listen to the value changes at the same time:
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.
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:
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.
Comments