This is the fifth and last part of a series of articles on Android Data Binding Library. Here are the others:
- Introduction to Data Binding
- Data Tags, Binding Expressions, Imports and Include
- RecyclerViews and Event Handling with Data Binding
- Binding Adapters and Observable Objects
- Two-Way Data Binding (You’re here)
And here is the github repository with several data binding samples(both in Kotlin and Java) as a companion to these articles.
In this fifth and last article, we’ll talk about two-way data binding and we’ll see a use case through our Two-Way Data Binding(Kotlin, Java) sample.
As we have seen up to now, we can show the user some data with Data Binding library. Now, consider this as a data flow from the application to user. We have also seen that we can get user input by setting listeners. Consider this second option as a data flow from user to the application. If, for some reason, you want to both show data and get user input over the same widget, in other words, if you need a two-way data flow, you can do them both in one line. This is what two-way data binding is about.
Let’s see some examples. Suppose you want to show some text on an EditText, but you also need to observe the changes applied by the user.
For setting the text, you can use:
android:text="@{viewModel.user.userName}"
And for observing the changes done by the user, you can set a listener like:
android:textAttrChanged="@{viewModel::onUserNameChanged}"
But you could also do both in one line. When you want it to work two-ways(to user and from user) you only need to add an “=” sign to the first expression:
android:text="@={viewModel.user.userName}"
This way, it will both show the data and also listen to changes by the user and update the underlying data accordingly.
But how does it happen? In the BindingAdapters section, I had mentioned that Data Binding Library has lots of built-in binding adapters for us to use. Well, it turns out that they have lots of built-in inverse binding adapters as well.
Inverse binding adapters work the opposite way for the corresponding binding adapters. For instance, for an EditText, “android:text” attribute will use the built-in binding adapter “setText()”. And if we declare it two-way by adding “=” symbol, it will use the associated inverse binding adapter, namely getTextString method(you don’t need to know these underlying methods), to listen to changes and update the referenced data accordingly.
Similarly, if we use “android:checked” attribute on a checkbox, it will use the binding adapter setChecked to set it as checked or unchecked. And if we make it two-way by adding “=” symbol, it will also use an inverse binding adapter to set an appropriate listener(like onCheckedChangeListener) and update the underlying boolean variable according to changes of the user.
Naturally, two-way data binding makes sense only for the widgets that can receive user input. Some examples are EditText, CheckBox, RadioButton, Spinner, SeekBar, NumberPicker, DatePicker.. For seeing a more comprehensive list and their corresponding binding and inverse binding adapters, scroll to the bottom of this guide.
Our two-way data binding sample demonstrate a use case for two-way data binding. It is a simplified inventory app, with a fragment to add or edit items and a fragment to show the list of items. The interesting parts of the application that are related to two-way data binding, are AddToyFragment, AddToyViewModel and the its xml layout fragment_add_toy.xml.
In the AddToyFragment, user input is taken by an edittext, checkboxes, radiobuttons and a spinner. But if you dig the code, you’ll see that we are not explicitly setting any listeners to these widgets or getting data programmatically with methods like getText, isChecked etc. All is done automatically by using two-way data binding. Similarly, when the same fragment is used for editing an existing item, widgets are populated by using data binding and when user makes changes, changes are again registered automatically.
Let’s see how this works. In the xml layout, we declared a variable of type AddToyViewModel. In the AddToyFragment, we pass an instance of AddToyViewModel to the binding instance. And in the AddToyViewModel, we have an instance of ToyEntry called toyBeingModified. In the xml layout, all widgets are bound to this toyBeingModified object, so that all changes done by the user are registered simultaneously on this object.
When AddToyViewModel is initialized, toyBeingModified is also initialized. If user is entering a new item(add case), toyBeingModified is initialized as an empty ToyEntry object(with default or null values). When user is editing a previously saved item(edit case), toyBeingModified is initialized as a copy of the chosen item. From there on, all the changes made by the user are simultaneously saved to this toyBeingModified object. In edit case, since toyBeingModified is initialized with some data, data binding populates the screen with the data. In add case there is no initial data, so nothing is shown. This doesn’t create a null pointer exception; the library deals gracefully with such cases.
When user clicks save, we only verify that toyName is not empty, then we simply save the toyBeingModified object.
Yes, it is that short! In fact, if you navigate through the repo, you can recognize that activities, fragments and even viewmodel has quite few lines of code(even in java version) That’s the beauty of data binding.
Now let’s look at the xml part of the code. Let’s begin by the most straightforward one: EditText.
Here, we are binding this editText in two-ways to the variable viewModel.toyBeingModified.toyName. This means, this editText will show the name of the toyBeingModified(if it exists) and if user modifies the name seen on this editText, it will listen to those changes and update the toyName of the toyBeingModified simulatenously.
This one was easy. However, everything is not always that straightforward. In our sample, edittext takes a String, toy name, and saves it as it is. But what if we had an EditText which takes a number and we wanted to parse it to an int or double before saving? Or look at the spinner. The attribute “android:selectedItemPosition” expects an integer, which corresponds to the position of the selected item in the list. But you probably would like to save the item that user picks, not its position on the list. In those cases, you can use converters.
In such cases, we use converters. A converter is in fact simply a helper method that you can import and use to convert the type of your parameters. What makes it different, is the ability to assign an “inverse method” so that this can automatically work both ways.
Let’s look at our spinner. In our sample, the spinner shows a list of genders: unisex, girl, boy. We want to get the selection of the user and save it in the gender field of our toyBeingModified object. ToyBeingModified is a ToyEntry and it has a gender property of type Gender, which is an enum. The spinner gives us information about the position of the item that user has selected. But we want to get the corresponding enum, instead of the position which is merely a number. Similarly, in edit case, when we want to show the previous selection of the user, we receive a Gender from the toyBeingModified object, but we need to convert it to the position, in order to tell the spinner which item to set as selected. So what we need to do is to convert the enum to position, and the position to enum (since this is two-way)
So here are our converter methods:
The first one, genderToPosition takes a Gender and return an integer, which is the ordinal of the selected Gender. Conveniently for us, ordinals of enums give an integer, which corresponds to the position on the list. If you are not familiar with ordinals, check out this documentation on enums. And the second method, does the exact opposite of the first method, it takes the position, and it gives back the corresponding enum.
Before explaining the InverseMethod annotation, let’s look at how we use it in xml:
The attribute “android:selectedItemPosition” expects an integer, which will be the position of the selected item. So the method genderToPosition() is our primary converter, we specify that one in the xml. We don’t specify the inverse method in the xml. However, since at the top of our primary converter method we specified the inverse converter with the annotation InverseMethod, when we use two-way data binding by adding “=” sign, the library will know which method to use in the other direction. So for populating the spinner with the previous data, it will use genderToPosition method. And when user makes a new selection, it will communicate the new selection to the viewmodel by using inverse method positionToGender.
There is a similar situation for the RadioGroup that we used in the sample. We ask the user whether this toy is bought or received. We again used an enum called ProcurementType with options BOUGHT and RECEIVED. However, with “android:checkedButton” attribute, when user makes a selection we receive a button id, which is an integer. We again have to convert this to ProcurementType. Similarly, when the application shows the previous selection of the user, the library receives a ProcurementType, but “android:checkedButton” attribute expects a button id. Thus again, we need converter methods.
And here are the converters we used for the radiogroup:
The method procurementTypeToButtonId is our primary converter. It converts the data in the model (whether the item was bought or received) to the corresponding checked button id and checks the correct button. This is the method we provide as the primary converter method in the xml:
android:checkedButton="@={BindingUtils.procurementTypeToButtonId(viewModel.toyBeingModified.procurementType)}"
But since we use that = symbol which signifies that we want two-way data flow and since we assign an inverse method to the primary method with the @InverseMethod annotation, the library knows that it should use the buttonIdToProcurementType method for converting the button id back to ProcurementType.
Now, let’s look at the checkboxes. For checkboxes, we can use the attribute “android:checked” in two-ways. This attribute expects a boolean, checks or unchecks the button accordingly. If you bind this attribute in two-ways to a boolean variable, it will also listen to the changes done by the user and update the given boolean accordingly.
In our sample, we have a group of checkboxes for possible categories that this toy belongs. And we wanted to get the list of all selected categories and save it on toyBeingModified object. There are different possible ways of achieving this. But I found it convenient to put these in a map, where keys give us the name of categories and values give us corresponding boolean telling whether that category is checked or not. This way, we didn’t need any converters on our checkboxes and it was easier to retrieve the list of categories, based on their boolean values.
If you remember from our previous article Data Tags, Binding Expressions, Imports and Include, we can use maps within binding expressions in this format:
android:checked = "@{map[key]}"
In our case, toyBeingModified has a field called categories which is a Map<String, Boolean>. Keys names are also located in the viewmodel as static constants. So here is the xml code for one of our checkboxes.
So, in edit case, this checkbox will show whether the corresponding category for this toy is selected or not. And if user checks or unchecks the checkbox, it will update the corresponding boolean value in the categories map.
For brevity, I shared only one checkbox. Others are similar. You can checkout the whole layout here if you wish.
If you found this “map thing” confusing, don’t worry. It was only an alternative strategy for not using lots of converters in this specific case. If you were able to understand what two-way data binding is about and how to use it, you’re good to go.
What’s Next?
I covered almost all the aspects of Android Data Binding library that you are likely to encounter and use. There are a few things I left out, since you’re least likely to need them. But for the most curious, I’ll share some extra resources.
Although the library provides us lots of bindings adapters and inverse binding adapters, it is possible to do your own inverse binding adapters as well. Custom inverse binding adapters are a bit more complicated than doing your custom binding adapters, which is the reason I left it out. But if you would like to build your own custom inverse binding adapters for your custom views, here is a good article about it, written by George Mount from Google Android team. In fact he has lots of cool articles about data binding on Medium, you might want to check it out.
There is also this article Data Binding - Lessons Learnt by Chris Banes who is also a member of Google Android team. He talks about some good practices related to data binding.
And of course, the official guides are quite comprehensive and covers more details than I was able to mention in these articles.