Two-way data binding and managing text inputs in Android
“A bidirectional way is partially a self-managed system.”
Generally, there is less you need to manage, when you use a two-way method. In this article you will be introduced to an amazing Android Jetpack library called Data Binding. I will try to begin with the issue and explain the main problems this library helps us with.
At the end of the story, You should be able to write your own android two-way data binding features.
Let’s clear a few things:
- It’s not a step-by-step to learn Data Binding in android, I will give you sources though.
- A simple scenario explained to illustrate the issue, but you can find more real world examples.
- You may correct/update me at any subject using comments section down there.
OK, Let’s assume that, we have pages containing dozens of input fields which is supposed to be filled by my app’s pity users! Consider following sketch.
When developing logic part(ViewModel) of such a form, there is a state in which we finally have to get the inputs and process/send/persist them. We used to have one and only way of fetching those data from view :
Classic way : “First, find views ,Second get the text value out,Third validations applied, Forth convert it to the desired export format for each input field! uh!”
Such a long and bug prone way to just get a form handled.
There even might be more complicated destination variable types than simple String or Integers such as Doubles and Longs. Therefore, you would have to format the entry as well. Now consider you wanted to implement a two-way connection with the data source(ViewModel/Repositories/Database). Now that I look at it, I see the whole thing as a nightmare but don’t get frustrated yet, since there are solutions available.
First approach : Data binding
What is it?
Data binding is a way of connecting views to the ViewModel and to each other. It lets us control view attributes via ViewModel nimbly. It’s designed to help developers focus on main logic by removing/reducing the boilerplate/repetitive/redundant code. If you are not familiar with it yet there is a perfect starting point for you: Just watch this video which is made by CodingWithMitch team.
As it’s stated in official Android developers documents :
“The Data Binding Library is a support library that allows you to bind UI components in your layouts to data sources in your app”
What it does for us?
In our case, despite all the benefits of Data Binding, it just helps us by providing us a wire to control EditText’s text attribute. This way, we are able to pull and push string text from/to EditTexts by modifying the testMessage from ViewModel.
So, I have my ViewModel:
// code removed for brevity
class viewModel:ViewModel() { var testMessage= "Not changing text"
Data binding changes this:
<EditText
// code removed for brevity
android:text=”@string/place_holder”/>
to this:
<EditText
// code removed for brevity
android:text=”@{viewModel.testMessage}”/>
The two-way wire
We can have a two-way wire using an equation symbol(=) to pull the content back to the ViewModel immediately after a change to the containing text:
<EditText
// code removed for brevity
android:text=”@={viewModel.testMessage}”/>
Non-String types?!
For now, We have solved automatic data loading and saving with almost no code! Brilliant! Wait a minute, What if testMessage is not a String variable?
// code removed for brevity
class viewModel:ViewModel() {var testMessage= 5 //Int var type -> Throws exception
var testMessage= 5f //Float var type -> Throws exception
In this case, You will face a runtime exception because EditText intrinsically assumes any Int input as a Resource and tries to find and display it but since that’s not what you meant it shows you a:
Resources$NotFoundException
Binding Adapter
The only way out of this is to write a Binding Adapter. To get started with adapters read this 4 min article(They’re lying, It took a day for me!) in medium.
Any ways, your binding adapter should look like this:
@BindingAdapter("android:text")
fun EditText.bindAnyToString(value: Any?) {
value?.let {
setText(value.toString())
}
}
We are casting entry to the String type, so EditText won’t look for a Resource id. Plus, No matter what variable type you pass to the EditText anymore, since it will be applying conversions to it in the adapter.
So, different incoming formats are handled simply, and you can also have different strategies when setting text:
@BindingAdapter("android:text")
fun EditText.bindObjectInText(value: Any?) {
value?.let {
if (value!=tag) { // Store the original value
tag = value // To prevent duplicate/extra modification
if (value is Float)
// Cast float primitives to 1 decimal number
setText(String.format("%1f", value))
else
setText(value.toString())
}
}
}
Nice, but what about the inverse data flow? What about pulling the content into ViewModel? Lets put an equation in XML to make it work two-way as we see every where in data binding world.
<EditText
// code removed for brevity
android:text=”@={viewModel.testFloat}”/>
Build your project and say hello to the build-time exception
Cannot find a getter for “android.widget.EditText android:text” that accepts parameter type “java.lang.Float”
Reason: EditText intrinsically returns String data type while ViewModel is waiting for Float primitive type. What to do then?
Inverse Binding Adapter
Inverse binding adapter is just a binding adapter with one important difference: data flow direction. It explains the way the data binding library can reverse the data modification direction. In other word, It’s how you can pull the text from EditText and tell the Data binding library to update ViewModel with a modified version of text.
Your inverse binding adapter should look like this:
@InverseBindingAdapter(attribute = "android:text")
fun EditText.getFloatFromBinding(): Float? {
val result=text.toString()
return result.toFloat()
}
This is how your EditText exchanges data up to ViewModel automatically. There is one more step to do it. Implement an inverse binding adapter for each returning type your ViewModel might be waiting for. We have an Int returning inverse binding adapter but we need a Float returning one as well. As a natural behavior, EditText String type is supported internally but you can have your own too. Your final inverse binding adapter should look like this:
@InverseBindingAdapter(attribute = "android:text")
fun EditText.getFloatFromBinding(): Float? {
val result=text.toString()
return result.toFloat()
}@InverseBindingAdapter(attribute = "android:text")
fun EditText.getIntFromBinding(): Int? {
val result=text.toString()
return result.toInt()
}
That’s it!
So, We have a form comprised of a number of input fields. We have a data source(ViewModel) also, which is supposed to feed the form and surly updated on every form modification. To get information persisted/processed/cooked we have all the ingredients required up and ready in ViewModel. Besides, Views get updates on ViewModel modification . No messy stuff, No repeating code, absolutely clean. Exciting!
Build and run your code and you’ll see the magic. Wiring is done by generated code behind the scene via annotation processors in build time. It’s worthwhile to take a look at generated code. It’s just generated java code to handle the job.
We tried to illustrate just one of the main issues with a world with no data binding library enabled, And provided a working way to you. Hence, there is one more approach to this issue which will make this whole process a lot simpler. I will discuss the second one as soon as possible as a medium story.
If you are looking for a working source code for this article, I’ve already implemented a custom view supporting two-way binding in this github repository. Feel free to use the code and reach me if there was a mistake or even questions.