; ;
Craft At WillowTree Logo
Content for craftspeople. By the craftspeople at WillowTree.
Engineering

Exploring Android's Navigation Architecture Component

Nish Tahir
Senior Software Engineer
May 14, 2018

As usual, Google I/O was packed full of announcements. However, this year there was an emphasis on making native Android development easier and more enjoyable. The Navigation Architecture Component, released as part of Android Jetpack and the new AndroidX package, aims to simplify the implementation of navigation in your Android app. This component and its guidelines offer a new look into how navigation should be implemented, including the suggestion for the use of single-Activity architecture as the preferred architecture for Android Jetpack moving forward. The Navigation Architecture Component also has support for fragments and deep links out-of-the-box, which we will see further helps create a cleaner, more predictable user experience.

Let’s start by taking a brief look at the Principles of Navigation as highlighted by the new guidelines:

1. The app should have a fixed starting destination

The user should always arrive at a fixed destination after launching the app. This excludes exceptions of one-time events such as login or terms and conditions. This lets users know what to expect when opening your app. Your starting destination is expected to be your home or main view. They should contain your primary navigation widgets such as a bottom navigation bar or navigation drawer.

2. Your navigation should be based on a Last in First Out (LIFO) stack

Your start destination should always be at the bottom of the stack. Navigating to a new destination should logically mean pushing the new destination to the top of the stack while hitting the back or up button should logically pop the given destination from the top of the stack. All operations that alter navigation or the navigation stack should operate on the top of the stack.

3. The up button should never exit your app

The up button should only be used to navigate within your application and logically pop items off of the navigation stack. If you are at your start destination, this means that your navigation stack should be empty and up button should not be shown.

4. The up and back buttons should function identically, for the most part

This should be true with the notable exception of situations where navigation stack is empty. Unlike the up button which should be hidden, the back button should close the app in these instances.

5. Deep linking to a destination should result in the same stack as normal navigation.

Deep links are an awesome way to take users directly to content that they want to interact with. They should be thought of as potential “quick navigation” methods through the app, not alternative entry points. In order to remain consistent with the principle that your app should have a fixed starting destination, deep linking should result in the same navigation stack that a user would create during normal navigation of your app.

Setup

Getting started with the tools starts off as you would expect. You need to add the navigation fragment and UI libraries to your project. They are available via the google() repository.

implementation 'android.arch.navigation:navigation-fragment:1.0.0-alpha01'
implementation 'android.arch.navigation:navigation-ui:1.0.0-alpha01'

There is also an optional additional step of including the safeargs navigation Gradle plugin. This plugin helps generate code that allows type-safe access to properties used in argument bundles. You will need to add the plugin to your project build.gradle classpath.

buildscript {
	...
	repositories {
    		google()
	}
	dependencies {
    		...
    		classpath 'android.arch.navigation:navigation-safe-args-gradle-plugin:1.0.0-alpha01'
	}
}

In your project build.gradle, you should now be able to apply the Gradle plugin as you normally would.

apply plugin: 'androidx.navigation.safeargs'

The Navigation Graph

The navigation graph lies at the core of the new navigation library. It describes how activities and fragments relate to each other and the ways in which you can transition between them.

Here’s an example of what this looks like.

<navigation
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:app="http://schemas.android.com/apk/res-auto"
	xmlns:tools="http://schemas.android.com/tools"
	app:startDestination="@+id/launcher_home">

	<fragment
    	android:id="@+id/launcher_home"
    	android:name="com.nishtahir.navigation.MainFragment"
    	android:label="@string/home"
    	tools:layout="@layout/fragment_main" />

	<activity
    	android:id="@+id/settings_activity"
    	android:name="com.nishtahir.navigation.SettingsActivity"
    	android:label="activity_settings"
    	tools:layout="@layout/activity_settings" />
</navigation>

We start with the navigation tag which contains a single Fragment and Activity. The root navigation element also declares a startDestination tag which brings us to the concept of destinations. A destination is described as

“… somewhere the user can go”

Programmatically, this would be a fragment or activity that is declared using its respective XML tags. Destinations have an id property which works as an identifier similar to those used in normal layout files as well as a name property which is used to reference the fragment or activity to instantiate.

You can create and manage new destinations in your navigation graph using the new Navigation editor available Android Studio 3.2 Canary 13 and higher.

Screen-Shot-2018-05-09-at-4.05.58-PM
It’s worth noting that the start destination is appropriately indicated by the home icon to the left of its ID

Now that we have destinations available on the screen, we can use Actions to navigate between them. Actions describe the intent to navigate between destinations. We can add a simple action that allows us to navigate from the launcher_home fragment to the settings_activity.

<fragment
android:id="@+id/launcher_home"
android:name="com.nishtahir.navigation.MainFragment"
android:label="@string/home"
tools:layout="@layout/fragment_main">

<action 
android:id="@+id/action_launcher_home_to_settings_activity"
app:destination="@id/settings_activity" />

</fragment>

We do this using the action tag along with an id and by providing the destination where we intend to navigate.

You can also accomplish this in the navigation editor by dragging an arrow from the launcher_home destination to the settings_activity destination.

other shot

By selecting the action in the Navigation Editor view, we can note that there are several options available to us to customize in the attributes panel on the right side of the screen. We are able to specify transition animations for properties such as the Enter and Exit transition animations, as well as the Pop Enter and Pop Exit animations. Fortunately, the navigation library comes with default animations for each of these options under the nav_default prefix; such as nav_default_enter_anim. While these are just simple fade animations, they are a welcomed convenience.

Navigating

Now that we have a basic graph setup, we can begin incorporating it into our code. We can do this by configuring an activity to act as the Host activity. This activity contains a NavHost which is an empty view that allows destinations to be swapped in an out as the user navigates through the app. This activity is expected to also contain some form of global navigation such as a Navigation Drawer or Bottom Navigation Bar.

A default implementation of NavHost is the NavHostFragment widget which you can add to your layout. We can see an example of this below.

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
	xmlns:android="http://schemas.android.com/apk/res/android"
	xmlns:app="http://schemas.android.com/apk/res-auto"
	xmlns:tools="http://schemas.android.com/tools"
	android:layout_width="match_parent"
	android:layout_height="match_parent"
	tools:context=".MainActivity">

	<android.support.design.widget.BottomNavigationView
    	android:id="@+id/bottom_nav_view"
    	android:layout_width="match_parent"
    	android:layout_height="wrap_content"
    	app:layout_constraintBottom_toBottomOf="parent"
    	app:menu="@menu/bottom_navigation" />

	<fragment
    	android:id="@+id/my_nav_host_fragment"
    	android:name="androidx.navigation.fragment.NavHostFragment"
    	android:layout_width="match_parent"
    	android:layout_height="0dp"
    	app:defaultNavHost="true"
    	app:layout_constraintBottom_toTopOf="@id/bottom_nav_view"
    	app:layout_constraintTop_toTopOf="parent"
    	app:navGraph="@navigation/navigation" />

</android.support.constraint.ConstraintLayout>

In your host activity, you want to use one of the setup utility methods available in the NavigationUI class. For this example, let’s set up our navigation graph to use a BottomNavigationView.

val navHostFragment = supportFragmentManager.findFragmentById(R.id.my_nav_host_fragment) as NavHostFragment
findViewById<BottomNavigationView>(R.id.bottom_nav_view)?.let { bottomNavView ->
	NavigationUI.setupWithNavController(bottomNavView, navHostFragment.navController)
}

Your bottom navigation view is populated by an XML menu which lists out the items as well as the destinations that they correspond to

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/launcher_home"
android:icon="@drawable/ic_home_white_24dp"
android:title="@string/home" />
<item
android:id="@id/launcher_dashboard"
android:icon="@drawable/ic_dashboard_white_24dp"
android:title="@string/dashboard" />
<item
android:id="@id/launcher_notification"
android:icon="@drawable/ic_notifications_white_24dp"
android:title="@string/notification" />
</menu>

Note that the IDs of the menu items should correspond to the IDs of the destinations in the navigation graph. This is how you bind destinations to menu items.

To manually navigate to a destination, you simply need to invoke navigate on your NavController function and pass in an action.

val controller = Navigation.findNavController(activity, R.id.my_nav_host_fragment)
controller.navigate(R.id.action_launcher_home_to_settings_activity)

A common use case for this would be navigating in response to a click event is a common use case, the Navigation class provides a convenience utility function to help

val clickListener = Navigation.createNavigateOnClickListener(R.id.action_launcher_home_to_settings_activity)
button.setOnclickListener(clickListener)

You can also navigate using the generated NavDirections directions classes. For example, to navigate from our MainFragment to our SettingsActivity,

val directions = MainFragmentDirections.action_launcher_home_to_settings_activity()
navController.navigate(directions)

Navigating this way allows you to decouple navigation logic from the rest of your code. Your activities and fragments don’t need to know anything about where the user coming from or where they are going next. They only need to focus on what they need to do. Decoupling in this way has other benefits. If your routing logic changes, you only need to modify the XML while requiring only minimal changes to your code.

Using safe arguments

The navigation architecture component comes bundled with a way for you to pass arguments through bundles in a type-safe way. It accomplishes this through code generation. Let’s take a look at this in action. Consider a fragment in our Navigation graph,

<fragment
android:id="@+id/launcher_notification"
android:name="com.nishtahir.navigation.NotificationFragment"
android:label="@string/home"
tools:layout="@layout/fragment_notification">
<argument
android:name="myarg"
app:type="string"
android:defaultValue="Home" />
</fragment>

Here we add the argument tag defining the name of our argument, its type as well as a default value if it was not passed.

In our fragment, the value is accessed using the generated Args class for the fragment

arguments?.let {
val args = NotificationFragmentArgs.fromBundle(it)
args.myarg
}

and that’s it. The fragment doesn’t need any prior knowledge of where the arguments come from. Let’s explore the generated NotificationFragmentArgs class

// This class is truncated for conciseness ...
public class NotificationFragmentArgs {
	private String myarg = "Home";
	public static NotificationFragmentArgs fromBundle(Bundle bundle) {
    	NotificationFragmentArgs result = new NotificationFragmentArgs();
    	if (bundle.containsKey("myarg")) {
        		result.myarg = bundle.getString("myarg");
    	}
    	return result;
	}

	public Bundle toBundle() {
    	Bundle __outBundle = new Bundle();
    	__outBundle.putString("myarg", this.myarg);
    	return __outBundle;
	}
	public String getMyarg() {
    		return myarg;
	}
	public static class Builder {
    	...
    	public Builder setMyarg(String myarg) {
        		this.myarg = myarg;
        		return this;
    	}
	}
}

The generated class does the wiring that you would typically have to do and exposes a lightweight typesafe API for you to work with. It also generates a builder which you can use to prepare your own instance.

Unfortunately, there is currently no way to mark fields as optional or not required and it would have been really nice to be able to create instances of the class using Kotlin’s named parameters as opposed to the builder but this is a good first step in the right direction.

Deep linking

Deep linking is a way for you to jump the user to specific parts of your app experience. Following the Principles of Navigation, we should also build the appropriate back stack that users would have created if they had navigated to the item manually. Fortunately, the Navigation Architecture component has got us covered in this area as well.

By adding the following to our XML,

<nav-graph android:value="@navigation/navigation" />

Intent filters, as well as other setup and wiring logic, will be generated for us.

Finally, to add deep links via web URLs, we only need to add the path data to a fragment or activity in our navigation graph.

<deepLink app:uri="willowtreeapps.com/careers" />

We can validate this by taking a look at our Merged Manifest.

<activity android:name="com.nishtahir.navigation.MainActivity" >
	<intent-filter>
    	<action android:name="android.intent.action.MAIN" />
    	<category android:name="android.intent.category.LAUNCHER" />
	</intent-filter>
	<intent-filter>
    	<action android:name="android.intent.action.VIEW" />

    	<category android:name="android.intent.category.DEFAULT" />
    	<category android:name="android.intent.category.BROWSABLE" />

    	<data android:scheme="http" />
    	<data android:scheme="https" />
    	<data android:host="willowtreeapps.com" />
    	<data android:path="/careers" />
	</intent-filter>
</activity>

Note that it not only added the appropriate intent filter actions and categories, but it also included the scheme for us automatically with both http and https protocols as well as properly configured the path.

To make sure we properly support up navigation, we need to override onSupportNavigateUp.

override fun onSupportNavigateUp(): Boolean {
	return navController.navigateUp()
}

Conclusion

The navigation library is a welcome addition to the architecture components. The ability to decouple routing logic from the view layer is something that Android has not been able to do historically. The inclusion of well-defined actions and type-safe arguments are excellent steps in providing a robust API.

I also strongly advise using the ktx module for navigation for kotlin projects. The extensions functions defined in the library makes for a much nicer API. For example, all of NavigationUI utility methods become extensions on the classes they operate on. There are also convenience extension functions for common tasks such as findNavController on Activity, Fragment and View.

A great example on using the navigation library is available here.

Nish Tahir
Senior Software Engineer

Recent Articles