The Android Jetpack Navigation Component has both revolutionized and standardized how routing is done on Android. Yet, as with every evolution, there is a learning curve to adapt existing conventions to new implementations. Let’s see how we can adapt this library to route users to a telephone number and web URLs.
I grouped handlers for both the telephone and web URLs together in the same article since they are similar. First, however, I’ll focus purely on an implementation for the telephone and the `tel:` protocol.
I want this extension to accept and route to any phone number that is handed to it. That way, if my app has the ability to route to several phone numbers, I only need a single node in my navigation graph to handle all of them.
In terms of code, I would expect this for the navigation graph:
Routing to this destination should look something like this:
It is also reasonable to expect a certain level of fault tolerance. Specifically, making a call from a device that does not have a telephone dialer, such as a tablet, shouldn’t fail silently and certainly shouldn’t cause an application crash. In this scenario, I’d like to display the number to the user so that he or she can place the call on a landline.
Now that the expectations are set, we can begin the implementation.
Building the Skeleton
Every custom navigator has a basic skeleton which we must implement before diving deeper. That foundation for our navigator looks like this:
Allow me to break down the relevant parts:
- `@Navigator.Name("telephone")` - Name of the node as it will appear in the navigation graph's XML file. In this case, I would use `<telephone />` to create this kind of destination.
- `Navigator<>` - All custom navigators must inherit from this class.
- `popBackStack() = true` - Since the dialer is most likely an external app, your graph is not responsible for the external back stack. Thus, whenever the dialer finishes itself and returns to your app, this destination is already popped off, and the navigator indicates `true` that the removal was a success.
- `@NavDestination.ClassType(Activity::class)` - Not strictly required, but I like to give the navigation graph an idea of what kind of view this destination will be. Since Android's dialer application is almost certainly external to your app, I consider it to be a generic `Activity` of some sort.
- `class Destination` - Not very exciting in this case. This class often maps out and fetches the custom attributes passed from the XML to the Kotlin (or Java) code for subsequent processing. However, since I opted to pass my properties dynamically via an argument, I don't need to specify any static argument properties here.
For the first iteration of our work, let’s ignore the edge cases and assume that there is always a handler the OS can use to make a call when given a phone number. By that expectation, we’ve already completed the necessary work for the `createDestination()` and `popBackStack()` methods. All that remains is the `navigate()` method.
If you recall from my XML sample above, I expect an `<argument />` with a name of `phoneNumber` to provide the data, I need to handle the routing properly. This information comes by way of the `args` parameter on the `navigate()` function. Obtaining the information looks like this:
Notice the `return null` at the end of the method. This is in place for the same reason as the `popBackStack() = true`. If I returned the `destination`, that would add this navigator to the current application's back stack and route to the external dialer. Under those circumstances, I left the state of my back stack alone by returning `null`. Not having to manage another back stack makes my job easier.
I see three glaring issues with my implementation above:
- What if the given phone number is null or blank?
- What if the number is invalid?
- What if the device doesn’t have a handler for the `tel:` protocol (i.e., no dialer app)?
The first and the last issue is simple to solve.
The second issue is a rather slippery slope. There are several libraries for Android which rise to the top for this purpose. They are PhoneNumberUtils and libphonenumber. However, based on my research, each library has a glut of gotchas that seem to either have no good reasoning behind them or expect a number formatted slightly differently, even if it is still valid. My advice is to forgo this kind of validation in the navigator. In all cases for apps I’ve developed, the provided phone numbers were pre-determined and either hard-coded or came from our company-maintained database. Truly, if we were handing out malformed phone numbers, we had bigger problems to deal with than letting them slip through our telephone navigator.
Before we can start using our new navigator, the navigation host must become aware of its presence. This is done by adding our navigator to the list of existing supported navigators.
To start, create a new navigation host called `CustomNavHostFragment` and implement it like this:
Finally, open up the layout file for the activity which holds your navigation host, and replace the `android:name` property with the fully-qualified class name to the navigation host you just created:
Now you are at liberty to route to your new destination type, just as prescribed at the beginning of this article. Let’s add this new destination to our `nav_graph.xml` file:
From here, we can route as we please:
Web URL Handler
For those of you with a web background, it may come as no surprise to know that the only difference between an HTTP handler and a telephone handler is the protocol. Web links use either `http:` or `https:`, while telephones use `tel:`. This section will cover the differences in the implementation and expectations for this kind of navigation destination.
I would expect this destination to process any URL handed to it via an argument like the telephone handler. Its signature on the navigation graph should appear as follows:
Routing to this destination should look something like this:
Having built an app used by hundreds of thousands of people, you get used to thinking about edge cases. This handler should be able to recover whenever there is no HTTP handler on the device. Yes. It happens. My first iteration of the Web URL handler did not account for this case and would end up crashing on devices that did not have a web browser. It’s a crazy world.
To start, copy the entire implementation of the `TelephoneNavigator` and adjust as shown below. In this case, since the code is so similar, I'll include only the differences so that you can change the relevant parts:
Once again, this section is a wash, rinse, repeat of the telephone navigator’s usage, albeit with the `<http />` tag instead of `<telephone />`. Keep in mind that you can add as many custom navigators to the `CustomNavHostFragment` as you please. Therefore, if your app needs both kinds of navigators, add them to the list and use them.
I hope you enjoyed my first article in this series. Hopefully, it gave you a good understanding of the basic techniques for adding your custom additions to the navigation toolset. These two are the only extensions that I expect to group because of their similarities. Forthcoming adaptation of the navigation library will offer more insights into the power we have as engineers to take this tool even further.
Here are some resources I used to help me gather my thoughts and build this kind of navigator: