Android Data Binding: Inverse Functions

Converting Both Ways

As I’ve written before, you can bind data to automatically set user input into a view model. For example, you might want to bind a user’s name so that when it is changed by the user, it is available immediately in the view model:

<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@={data.firstName}"
android:textSize="16sp"
/>

Directly binding a property to an attribute works well when the view model’s type matches the attribute type. Unfortunately, this isn’t always the case. If the user enters a numeric value in an EditText, it won’t automatically be converted to an integer.

One-Way Conversion Functions

With one-way data binding, converting from the view model to the View’s attribute type is pretty easy — just use a static method. For example:

<TextView android:text="@{Integer.toString(user.age)}" .../>

Or, you could use string concatenation as a shortcut:

<TextView android:text="@{`` + user.age}" .../>

Probably the best way to do it is to use string formatting:

<TextView android:text="@{@string/ageFormat(user.age)}" .../>

This allows the formatting to take localization into account.

Two-way Conversions

Two-way conversions are more difficult because there is no inverse for a static method and certainly no conversion for arbitrary string formats. However, it is a very common requirement, so some conversion utilities were added.

String-to-primitive conversions are very common and can be done with a string concatenation syntax with two-way data binding expressions, but only with the empty string:

<EditText android:text="@={`` + user.age}" .../>

That works pretty well for the simplest conversions. Sometimes the user types in something invalid (empty text, for example), and these simple conversions handle that by simply not updating the view model with invalid conversions.

Many times applications need different conversions. You may need to convert from a selection integer to an enum, for example. With one-way binding, this would be something like:

<Spinner
android:selectedItemPosition="@{Conv.toInt(user.numberType)}"
.../
>

Here, the Conv class contains:

public class Conv {
public static int toInt(PhoneNumberType phoneNumberType) {
return phoneNumberType.ordinal();
}
}

But when you want to use a two-way conversion, you need to provide the function that converts from the integer back to the enum. New in Android Studio 2.3, you can now use the @InverseMethod annotation to declare the inverse of a conversion method:

public class Conv {
@InverseMethod("toPhoneNumberType")
public static int toInt(PhoneNumberType phoneNumberType) {
return phoneNumberType.ordinal();
}

public static PhoneNumberType toPhoneNumberType(int ordinal) {
return PhoneNumberType.values()[ordinal];
}
}

And the expression can now be made two-way:

<Spinner
android:selectedItemPosition="@={Conv.toInt(user.numberType)}"
.../
>

Here is a slightly more complex example. Let’s say that you want to use the primary locale to display and parse a double from an EditText. The conversion functions would be:

public class Converter {
@InverseMethod("toDouble")
public static String toString(TextView view, double oldValue,
double value) {
NumberFormat numberFormat = getNumberFormat(view);
try {
// Don't return a different value if the parsed value
// doesn't change
String inView = view.getText().toString();
double parsed =
numberFormat.parse(inView).doubleValue();
if (parsed == value) {
return view.getText().toString();
}
} catch (ParseException e) {
// Old number was broken
}
return numberFormat.format(value);
}

public static double toDouble(TextView view, double oldValue,
String value) {
NumberFormat numberFormat = getNumberFormat(view);
try {
return numberFormat.parse(value).doubleValue();
} catch (ParseException e) {
Resources resources = view.getResources();
String errStr = resources.getString(R.string.badNumber);
view.setError(errStr);
return oldValue;
}
}

private static NumberFormat getNumberFormat(View view) {
Resources resources= view.getResources();
Locale locale = resources.getConfiguration().locale;
NumberFormat format =
NumberFormat.getNumberInstance(locale);
if (format instanceof DecimalFormat) {
DecimalFormat decimalFormat = (DecimalFormat) format;
decimalFormat.setGroupingUsed(false);
}
return format;
}
}

You’ll notice that the conversion methods take three parameters instead of just one. You can pass any number of parameters, and the inverse method must take the same number of parameters. All but the final parameter of the conversion and its inverse are the same. The final parameter type of the conversion (here, double) and the return type of its inverse must match. Likewise, the final parameter type of the inverse (here, String) must match the return type of the conversion method.

The other parameters passed are the same for conversions both ways. Converting from double to String should be fairly trivial, but the toString() method does one extra step. When converting, if there is no change in the parsed value, the String value doesn’t change. When the text is “10.0” and the user deletes “0” to make the text “10.” the parsed value is “10” and you don’t want the text to change to “10” during the user edit.

It also seems a little strange to pass oldValue when it isn’t used in the toString() method. However, that value is used in the inverse method so it must be passed to the conversion method as well.

The inverse method, toDouble(), must parse the String to a double. It is possible that the value is invalid and we must warn the user. This converter shows the error on the EditText when it can’t parse the double. The converter must also return a double value, so when it can’t parse the text, the old double value is returned.

The binding expression using this converter is:

<EditText
android:id="@+id/total"
android:inputType="numberDecimal"
android:text="@={Converter.toString(total, user.val, user.val)}"
...
/>

Summary

Android Data Binding provides a couple of ways to convert with two-way data binding expressions. Using empty string concatenated with a primitive will give you a quick-and-dirty two-way binding for use with EditText. The other way is to provide your own conversion methods, annotated with @InverseMethod. As shown above, inverse methods allow you to do some pretty powerful things during conversion, including setting error text. The two-way conversion functionality should make application development a little easier to pull data from the user.

Show your support

Clapping shows how much you appreciated George Mount’s story.