Two-way Databinding with a custom property in Android

Learn how to setup two-way databinding with custom properties or custom views.

I know you will try to skim it but I suggest you to read everything to really understand how it works to apply it in your own project, but if you just want the code, you can find it in the end of the article.

Requirement:
The view that you will bind must have a change listener, such as the EditText that has the TextWatcher or the Spinner that has setOnItemSelectedListener. If it is a custom view that doesn't have it, you'll need to adapt it, but I won't cover it in this tutorial.

Let's code!

1. Create a name for the attribute you want to bind and add it to your xml. Don't forget the = sign.
<com.blackcat.currencyedittext.CurrencyEditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:realValue="@={presenter.amount}"
/>

In this example I used a customview, but the process is the same for android view. You need to add the prefix app: to the attribute name you decide. The name I chose for mine was app:realValue.

Don’t forget to assign it to a variable that will hold the value (in this example:amount).

2. Make sure you create the getter and setter for this variable on your presenter or model. In my case:
public void setAmount(double amount) {
this.amount = amount;
}
public double getAmount() {
return this.amount;
}
3. Create a Binder class with the bindingAdapters

You can create this class with any name and location within your project and it doesn't extend anything. The important part here is the annotation.

I created the CurrencyBinder and added the following methods:

1. The first method will be responsible to set a listener to the view

@BindingAdapter(value = "realValueAttrChanged")
public static void setListener(CurrencyEditText editText, final InverseBindingListener listener) {
    if (listener != null) {
editText.addTextChangedListener(new TextWatcher() {
...

@Override
public void afterTextChanged(Editable editable) {
listener.onChange();
}
});
}
}

This method can have any name but it is important to add the annotation @BindingAdapter(value = "realValueAttrChanged") with the value being the name of the attribute with the suffix AttrChanged. Ex: attribute name is selectedItem. So the value you should use is: selectedItemAttrChanged.

The first parameter will be the view you insert your binding and the second parameter must be the final InverseBindingListener listener. When the change occurs you must call the listener.onChange() so it knows that something has changed and calls the 3rd method we will create.

2. The second method is the one that sets the input with the value coming from the model/presenter.

@BindingAdapter("realValue")
public static void setRealValue(CurrencyEditText view, double value) {
if (!isSameValue()) {
view.setText(String.valueOf(value * 10));
}
}

This @BindingAdapter("realValue") must have the name of the attribute we created before.

This also can have any name but I suggest to keep it like a setter. The first param is the view and the second one is the value that will be sent from the model/presenter. Here you will have to set the new value to the view and it is important to check if the new value is the same as the old one to avoid getting into a loop.

3. Finally the third method is the getter that will call our model/presenter setAttribute

@InverseBindingAdapter(attribute = "realValue")
public static double getRealValue(CurrencyEditText editText) {
return editText.getRawValue() / 100;
}

This method must be annotated with @InverseBindingAdapter(attribute = "realValue". As you can see the attribute is the attribute name we've been using since the start.

That's it! The final class:

public class CurrencyBinder {

@BindingAdapter(value = "realValueAttrChanged")
public static void setListener(CurrencyEditText editText, final InverseBindingListener listener) {
if (listener != null) {
editText.addTextChangedListener(new TextWatcher() {
@Override
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}

@Override
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
}

@Override
public void afterTextChanged(Editable editable) {
listener.onChange();
}
});
}
}

@BindingAdapter("realValue")
public static void setRealValue(CurrencyEditText view, double value) {
if (!isSameValue()) {
view.setText(String.valueOf(value * 10));
}
}

@InverseBindingAdapter(attribute = "realValue")
public static double getRealValue(CurrencyEditText editText) {
return editText.getRawValue() / 100;
}
}