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'
implementation "android.arch.navigation:navigation-fragment-ktx:$navVersion"
implementation "android.arch.navigation:navigation-ui-ktx:$navVersion"

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.

<navigation app:startDestination="@id/main_fragment">
<fragment
android:id="@+id/main_fragment"
android:name="com.marianeum.navigation.MainFragment"
android:label="@string/title_main"
tools:layout="@layout/fragment_main" >
<action
android:id="@+id/open_details"
app:destination="@id/details_fragment" />
</fragment>
<fragment
android:id="@+id/details_fragment"
android:name="com.marianeum.navigation.DetailsFragment"
android:label="@string/title_details"
tools:layout="@layout/fragment_details" />
</navigation>

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.

<FrameLayout>

<fragment
android:id="@+id/nav_host"
android:name="androidx.navigation.fragment.NavHostFragment"
app:defaultNavHost="true"
app:navGraph="@navigation/nav_graph"/>

</FrameLayout>

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):

findNavController().navigate(R.id.open_details)

Note that all you define is the id of the action. Which fragment you navigate to is already defined in the navigation graph:

<action
android:id="@+id/open_details"
app:destination="@id/details_fragment" />

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 Intent or 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:

classpath "android.arch.navigation:navigation-safe-args-gradle-plugin:$navVersion"
apply plugin: 'androidx.navigation.safeargs'

The 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 classpath.

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 NavDirections interface:

val direction = MainFragmentDirections.open_details(id)

The plugin generate a class using the name of the fragment and appending Directions (MainFragmentDirections), as well as a class for the action (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)
findNavController
().navigate(direction)

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 Builder available:

val args = DetailsFragmentArgs.Builder("id1").build()

Unfortunately the 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:

androidTestImplementation "android.arch.navigation:navigation-testing:$navVersion"

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 Activity.

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 activity:

<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:

<deepLink app:uri="google.com/{id}"/>

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 Activity:

override fun onSupportNavigateUp() = findNavController(R.id.nav_host).navigateUp()

What else?

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