A problem like Navigation — Part 1
Navigation on Android can get quite complicated — from passing data to handling the back stack there’s many things to watch out for. There’s many solutions out there trying to solve that problem — often working with a single Activity, but none of them satisfied me so far. They were either too complicated or didn’t handle things like restoring state.
So when Google announced the new Navigation Architecture Component I got very excited and decided to play around with it over the weekend. This is the first alpha release, so things are still a bit rough and might change a lot over time, but so far it’s looking promising.
To be able to use the navigation component you’ll have to add two dependencies:
def navVersion = '1.0.0-alpha01'
There’s currently no androidx versions for these, so if you migrated already you might have to roll back.
How does it work?
The new component works with a single Activity architecture using Fragments. The heart of it is the navigation graph. This is an xml file where you can define your fragments and actions to navigate between them.
Note: I am stripping out a few things from the examples to make them more readable.
Here’s an example of a simple graph with two fragments — main and details and an action to move from main to details. There is also a UI representation of this graph, but unfortunately I ran into a bug in Android Studio that caused it not to load.
At the top you can find the
startDestination — this defines on which page the app should start on.
The other part you have to configure is the Activity.
All you have to add is a
NavHostFragment linking to the
navGraph defined above. To then navigate from one place to another you have to add the following line in your
Fragment (in a click listener for example):
Note that all you define is the
id of the action. Which fragment you navigate to is already defined in the navigation graph:
This is great — the navigation gets defined in a single place and if you change where an action navigates to you just have to update the xml — the code can stay the same.
What about passing data?
Passing data between Activities and Fragments has bothered me for a while. You can add extras to a bundle, but where do you define what goes in it? Maybe you defined a static method to create the
Fragment, maybe you had a centralised navigator, but still: nothing would stop you from passing any arbitrary extras into the
Bundle. When reading from the
Bundle you had to know the extras and their type again. There was no real contract between the origin and the destination.
The navigation component tries to solve that using the safe args plugin:
apply plugin: 'androidx.navigation.safeargs'
classpath needs to be defined in your top level gradle file, whereas the plugin needs to be applied in your module or app. Note: the documentation currently has the wrong package for the
Looking at the
DetailsFragment above — let’s say we want to pass through an
id to it. We just have to add this argument inside the
fragment of the navigation graph:
<argument android:name="id" app:type="string"/>
When rebuilding the app this will generate a few things. Firstly for the origin a direction class implementing the
val direction = MainFragmentDirections.open_details(id)
The plugin generate a class using the name of the fragment and appending
MainFragmentDirections), as well as a class for the
open_details) with the arguments required (
id). To navigate to it all you just have to call the
navigate method with the direction:
val direction = MainFragmentDirections.open_details(id)
Again — you don’t have to know about the destination here. If the destination changes nothing will change here (unless any of the extras change).
To extract the information on the destination side an
Args class is generated — again using the name of the fragment — and appending
Args. To extract the
id all you have to do is this:
val id = DetailsFragmentArgs.fromBundle(arguments).id
You don’t have to know about what the origin was or what the extra keys were — all of that is done under the hood for you.
Unfortunately it is currently not possible to define some arguments as required or optional (maybe your
id should never be null, but others can be)— you can define a
defaultValue in xml, though, which helps in some cases. It’s definitely a good first step into the right direction.
Another advantage is testing. Let’s look at the
DetailsFragmentArgs from above. You might want to pass the arguments to your presenter or
ViewModel — especially for more complex arguments. The good news is: once the arguments are created from the bundle it is just a simple Java object, making it easy to be used for unit tests. To create an arguments object for testing there’s a
val args = DetailsFragmentArgs.Builder("id1").build()
Builder doesn’t have an empty constructor at the moment — this would be great for testing more complex arguments.
On the origin side it could be useful to let the
ViewModel or presenter create the
NavDirections objects. This means all the navigation logic would be there, passing the
NavDirections object to the
Fragment which just has to call the
navigate method using the object passed.
Unit testing these object isn’t trivial, though. Currently they don’t have an
equals method and don’t expose the individual arguments as public getters. All they expose is a
getArguments method, which is a
Bundle and therefore not unit testable, as well as the action id. So you either have to use mocks or reflection to compare the fields — neither is great.
If you’re writing android tests there’s a few helpers in this test dependencies, so be sure to check it out:
What about deep linking?
Deep linking is another thing that was relatively complicated. You had to define
intent-filters in the
AndroidManifest and then manually extract any data, like an id, from the
uri in your Activities. This meant you had two different ways of extracting the data — one for a deep link and one for navigating from a different
The navigation component solves that problem too. First, to get deep linking working with it, you have to add the following to the
AndroidManifest within the
<nav-graph android:value="@navigation/nav_graph" />
This will link up all your deep links inside your navigation graph and generate the intent filters for you.
To be able to deep link into a fragment you have to add the following to the
fragment in the navigation graph:
This will generate the intent filters for you (https and http) in the
AndroidManifest and navigate to the correct fragment with the correct arguments (in this case the
id). As we already defined the arguments for the
DetailsFragment we have to do no extra work to handle this deep link. It’ll also handle the up and back button for you, as long as you override the
onSupportNavigateUp method in the
override fun onSupportNavigateUp() = findNavController(R.id.nav_host).navigateUp()
Right now I have only scratched the surface of this — there’s a lot more to look into — especially for more complex cases. But it seems a lot simpler — a lot of the boiler plate code for navigation is taken care of. So far it looks very promising and I’m looking forward to learn more about it! If you’re interested be sure to check out this Google I/O video as well as the documentation.
Liked it? Continue with Part 2