Hands on Jetpack Compose VisualTransformation to create a phone number formatter
At Aircall, we are currently rewriting our design system with Jetpack Compose, the new UI framework made by Google. One of our UI component is a text input that formats a phone number to its international format according to its origin country. With the View UI toolkit, we can use a PhoneNumberFormattingTextWatcher, which is available since API 1, to format the input text. But no equivalent has been implemented with Jetpack Compose so far. In this article we are going to cover how we managed to create a reliable alternative for Compose with VisualTransformation.
First, let’s have a look at the VisualTransformation interface to understand how it works under the hood and how you can use it to format the input text of a TextField as you wish.
In order to filter the text, you will need to implement the filter() function which takes the original text to transform as a parameter and returns a TransformedText where all the magic happens! If we take a look at the constructor it takes a text (which will be transformed) and an OffsetMapping which provides a bidirectional mapping between the original text and the new one. Which means that we’ll need to implement the transformation from and to the original.
Let’s have a look at the password transformation that the framework provides to see the OffsetMapping in action:
Here, the implementation is pretty straightforward as we just need to transform the input text where every character will be replaced by a mask, so the text will have the same length. The transformed text will contain the mapped text and the offsetMapping will handle the transformation. Here, as we don’t have any extra character to be displayed in the returned text, the two functions will return the original offset.
The two transformations can become a bit more complex if we need to display additional characters in the formatted text like a credit card format for example.
Here is the implementation of the OffsetMapping if we want to add a dash symbol every 4 digits. When we’ll be transforming the original text, we’ll return the original offset + the number of dash as soon as we reach a new block of 4 digits. And for the opposite, we are going to return the offset of the transformed text minus the count of dash when we reach the related 4-digits block.
Now that we have a good overview of the VisualTransformation interface to build our international phone number formatter. Let’s see how we can actually achieve the formatting thanks to libphonenumber library.
Format text as a phone number
In order to format the input text of the TextField, we used the following library developed and maintained by Google.
If we look at the implementation of the PhoneNumberFormattingTextWatcher we can see that it is using an internal version of the phone number library to format the input text. And this will be a strong base to format the original text.
To format the text while typing, we are going to use a AsYouTypeFormatter which comes from the PhoneNumberUtil instance of the library. Do not forget to use the right Locale when building your new instance in order to target the desired format for the typed phone number.
The format will depend on the cursor location and the last non separator character (a.k.a the last digit of the typed number).
To properly reformat the input we looked at the implementation of the reformat() method from PhoneNumberFormattingTextWatcher in the Android Sources and we adapted it to fit our use case here:
Now we have reformatted the input text as we wanted, we are going to compute the offsets needed for the VisualTransformation in order to have a proper cursor management in the input.
To compute the offsets, we are going iterate on each character of the formatted text. First, we are going to check if the character is a separator or not in order to store the index of this character in a dedicated list, one for the original to the transformed text and another for the other way.
When processing the formatted text, we are going to keep the count of the special characters that are present in the phone number. This will help us to store the right index in order to keep the right place of the cursor when transforming from its formatted form to the original one.
In other words, when you will want to edit a digit in the number you will be able to click on it and the cursor will be at the right place.
Now we have everything we need to complete the phone number formatter, let’s see how we can build this new PhoneNumber input and how it looks like in the design system.
Phone number formatting with Compose
If we put all the pieces together, we have a brand new phone number VisualTransformation for our TextField:
And here is what it looks like in the new Aircall design system with the PhoneNumberTextField which is composed of an Image, to display the country flag, and a TextField to display the formatted phone number.
With Compose, we can easily access and set the value of the text transformation parameter with a new definition of PhoneNumberVisualTransformation() as you can below.
Then, we can build a custom Composable where it can detect the country code and set the corresponding flag or choose directly the country flag in order to avoid typing the international extension in the input text. Thanks to the VisualTransformation, the text input will be formatted in real time.
Creating this phone number formatter with Jetpack Compose has been very interesting in many ways: understand new parts of the framework with VisualTransformation or explore the Android source code to learn how things works under the hood. Compose allowed us to build highly customizable components very easily even if we had to reimplement something from the Android core API.
This formatter is not yet perfect and can be improved like when you long press on the Backspace button of the keyboard, the phone number is not fully erased.
Big thanks to the Aircall Android team for bringing this great Composable to life! 🔥
Do not hesitate to ping me on Twitter if you have any question 🤓