Navigation Architecture Component for the Rest of Us

James Shvarts
ProAndroidDev
Published in
11 min readSep 9, 2018

--

Photo by Leon Seierlein on Unsplash

Updated for navigation component v2.2.1 on April 5th, 2020

Recently I had a chance to try out the new Navigation Architecture Component library and wanted to share what I learned. I will walk you through applying the new navigation architecture to a Notes/todo app. Using a basic Note-taking app is an effective way to learn a new library or framework without requiring knowledge of any business domain.

The app is in Kotlin and follows the popular MVVM architecture with help of other Jetpack libraries such as ViewModel and LiveData.

Complete source code for this article is available on Github. Feel free to browse the source code directly or scan the pull requests (there is one for each feature).

Here are some things we’ll be looking at:

  1. General project and NavHostFragment setup
  2. Nav Drawer setup
  3. Setting up domain layer
  4. Implementing note list, detail, edit and delete functionality
  5. Using the navigation drawer
  6. Setting up deep links
  7. Adding transition animations
  8. Managing top-level destinations with AppBarConfiguration
  9. Bonus: migrating from support library to AndroidX

General project and NavHostFragment setup

To use the new navigation editor, you will need to use Android Studio Canary. Some of the New Project templates already include basic Navigation included but I’d recommend starting from scratch — it’s the best way to learn.

Create a new navigation resource folder and an empty navigation graph XML file.

Keeping your navigation graph in xml makes it easy to diff navigation changes over time, i.e. enables better code reviews.

We will be passing arguments between the screens so we need to add this Gradle plugin to the top-level build.gradle (complete build.gradle can be found here).

In order to pass arguments between your Fragments (assuming you are building a single Activity app), you will also need to add the safeargs plugin to app/build.gradle:

We will use Kotlin library versions to take advantage of the extension functions. Add these dependencies to app/build.gradle. We’ll need other dependencies as well to complete various aspects of the app so check out the complete app/build.gradle here.

Google recommends a single Activity with multiple Fragments when using the new Navigation component. Navigating via Fragments simplifies the lifecycle greatly as you don’t have to deal with complexity of interaction between Activity and Fragment lifecycles. It also makes applying shared element transitions and transition animations very easy, as we’ll see below. You can use Activities as your destinations but for the reasons above, Google’s recommendation is to use Fragments. Activities as destinations make sense when your navigation flow involves interaction with other apps.

One of the biggest benefits of working with the new Navigation Arch Component is that we can edit and see our navigation graph visually in the Design View. When we are done with this tutorial, we will have the following flow:

The arrows represent actions. It is easy to see that from the notesFragment (start destination) we can go to either noteDetailFragment or addNoteFragment. Once on noteDetailFragment, we can go to either editNoteFragment or deleteNoteFragment.

Let’s create our activity_main.xml layout first:

This layout has a Nav Drawer which will be automatically synchronized with our navigation graph (choosing a menu item in the nav drawer will take us to that navigation destination). When the backstack is empty, the Up indicator will be replaced by the hamburger menu icon. There is a nice built-in animation to toggle between them: no need to integrate the ActionBarDrawerToggle yourself.

The NavHostFragment is the fragment container outlined below by the red box which is used as a container for different fragments within the navigation flow.

Add our one and only Activity in this app:

NavController

At this point we have a NavController set up which will work with the currently running NavHostFragment to enable navigation actions including deep linking, maintaining backstack, managing action bar and the nav drawer icon.

NavController manages all things navigation for us while we never have to touch FragmentManager or FragmentTransaction directly!

Let’s look at each function in the MainActivity separately:

  1. setupActionBarWithNavController ensures that the title in the action bar will automatically be updated when the destination changes (assuming that android:label values are set up). In addition, the Up button will be displayed when you are on a non-root destination and the hamburger icon will be displayed when on the root destination.
  2. setupWithNavController ensures that the selected item in the NavigationView will automatically be updated when the destination changes.
  3. navigateUp ensures that the menu items in the Nav Drawer stay in sync with the navigation graph.

These functions belong to androidx.navigation.ui.NavigationUI class found in the navigation-ui dependency.

NavigationUI provides support the following top app bar types:

For now our navigation drawer is empty but we’ll populate it soon.

Set up domain layer

Let’s throw together a basic set of domain classes that the rest of the app can use. Our domain consists of only Note model class and NotesManager singleton object which allows reading, updating and deleting notes.

Implement Note List screen

Update the mobile_navigation.xml to include the new Fragment:

NoteListFragment will be the default Fragment destination in our app (it will be the first screen shown when the app loads) as set in the app:startDestination attribute. Note that the android:label will be visible in the ActionBar.

Actions aka Routes

The NoteListFragment destination defines two available actions. Hopefully, you will find my naming conventions self-documenting.

  1. action_notes_to_addNote will take the user to AddNoteFragment. This action will be used when the user clicks on the FAB.
  2. action_notes_to_noteDetail will take the user to NoteDetailFragment. This action will be used when the user clicks on an item in the RecyclerView.

There are a few things about the actions that you can control right in the navigation graph xml:

Add the ViewModel which will interact with NotesManager to load notes.

Finally, add the NoteListFragment which will observe observableNoteList from our ViewModel

As you may recall from above, there are 2 possible navigation actions that can occur in this Fragment and here is how we use them.

fab.setOnClickListener {
val action = NoteListFragmentDirections
.actionNotesToAddNote()

findNavController(it).navigate(action)
}

and

private fun onNoteClicked(note: Note) {
val action = NoteListFragmentDirections
.actionNotesToNoteDetail(note.id)

findNavController().navigate(action)
}

Note that the action names are converted into camelCase for you so they conform to the standard Java naming conventions.

Also note that the actionNotesToNoteDetail takes a mandatory note.id param which will be set up below. If we fail to include the param, we’ll get a compilation error.

Implement Note Detail screen

Update the mobile_navigation.xml to include the new Fragment:

Here we define that NoteDetailFragment expects a mandatory noteId param of type integer. After all it would not make sense to start this Fragment without the noteId argument — we would not know what to load!

If this parameter was optional, we’d use the defaultValue attribute like so:

<argument
android:name="noteId"
app:argType="integer"
android:defaultValue="0" />

The following actions are logical navigation choices available from the NoteDetailFragment:

  1. action_noteDetail_to_editNote
  2. action_noteDetail_to_deleteNote

Here is the actual NoteDetailFragment code:

Note that the parameter noteId is retrieved by this Fragment using

private val args by navArgs<NoteDetailFragmentArgs>()

which is a static auto-generated and imported method on the NoteDetailFragmentArgs.

If you look at the build folder, you can see all args-related code generated for you by the safe-args-gradle-plugin. This plug-in gives us compile-time checks for parameters presence and correct types used.

Here is how navigating to the Edit Note screen is accomplished (again, the noteId argument is essential for the EditNoteFragment)

noteId argument is extracted using by navArgs as well:

private val args by navArgs<EditNoteFragmentArgs>()

and then passed in as a param when creating actionNoteDetailToEditNote:

editNoteButton.setOnClickListener {
val action = NoteDetailFragmentDirections
.actionNoteDetailToEditNote(args.noteId)

findNavController(it).navigate(action)
}

Similarly, the DeleteNoteFragment needs the noteId to know what to delete.

deleteNoteButton.setOnClickListener {
val action = NoteDetailFragmentDirections
.actionNoteDetailToDeleteNote(args.noteId)
findNavController(it).navigate(action)
}

This is the power of SafeArgs Gradle plugin for the Navigation Component. Lyla Fujiwara described it perfectly:

Generates classes based off of your navigation graph to ensure type-safe access to arguments for destinations and actions.

Implement Delete Note screen

We’ll skip the AddNoteFragment and EditNoteFragment (you can check them out on Github) and move on to the DeleteNoteFragment which introduces new ways to navigate in addition to what we’ve covered above. It is implemented as a regular Fragment but in ideal world the Confirm Delete functionality should be implemented as a DialogFragment. Unfortunately, this is currently not supported by the library.

Update the mobile_navigation.xml to include the new Fragment:

As you see, this Fragment also needs a noteId argument to determine which note to delete.

Here is the DeleteNoteFragment itself:

When the Cancel Button is clicked, we pop back stack (navigate back up the navigation hierarchy back to the Note Detail screen)

cancelDeleteButton.setOnClickListener {
findNavController(it).popBackStack()
}

When the note is successfully deleted, it does not make sense to pop back stack (to navigate back up to the Note Detail screen) since the note is gone from our data source by then. Instead, we navigate all the way back to the starting screen of the app (Note List screen) by popping back stack to that particular destination.

confirmDeleteButton.setOnClickListener {
viewModel.deleteNote(args.noteId)
}

Add navigation drawer menu item

Let’s create an empty SearchNotesFragment and add it to the mobile_navigation.xml and the nav drawer.

Now the menu_nav_drawer.xml can be edited to include the new destination:

And that’s it! The work we did earlier in the MainActivity to link the NavController to the Nav Drawer NavigationView will guarantee that the navigation drawer always stays in sync with the current navigation selection.

Set up deep links

There are two types of deep links:

  1. Explicit (NavDeepLinkBuilder can be used to programmatically set up a deep link)
  2. Implicit (deep links are configured declaratively in our mobile_navigation.xml)

Let’s take a look at an implicit deep link. Assume that we need to deep link directly into a Note Detail screen to a given note by a noteId. We can simply add the following tag within the fragment definition for NoteDetailFragment:

<deepLink
android:id="@+id/noteDetailDeepLink"
app:uri="notesapp://notes/{noteId}" />

As long as the name in curly brackets ({noteId}) matches the name of the argument we defined earlier for this Fragment, things should just work! Then in AndroidManifest.xml, include the nav-graph tag and point to our navigation graph:

<activity android:name=".presentation.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<nav-graph android:value="@navigation/mobile_navigation" />
</activity>

No need to set up intent filters manually! That will all be done for you by the Navigation library by way of AndroidManifest merge.

Testing deep links

To test my deep link, I executed the following adb command after creating several notes in the app:

adb shell am start -a android.intent.action.VIEW -d "notesapp://notes/2" com.jshvarts.notesnavigation

I was successfully deep linked directly into the Note Detail screen with note 2 loaded.

Besides being super easy to set up deep links, a huge benefit you get is consistent navigation. There is no need to manually synthesize the backstack.

Clicking the Up button or hitting the Back button after deep linking, takes you to the Note List screen because the deep link is set up within the navigation graph which is aware that the NoteListFragment is a parent of the NoteDetailFragment. No need to manually code the Up and Back button behaviors anymore.

Add transition animations

Let’s add a couple of animations to the NoteListFragment. We can do declaratively in the navigation graph xml.

The animations apply to an action. In this case, we will animate the transition from the Notes screen to a Note Detail screen. To do so, you can modify the navigation graph file manually or use the excellent navigation editor in Android Studio.

  1. enterAnim specifies how the Note Detail screen will be animated when it is navigated to.
  2. exitAnim specifies how the Notes screen will be animated when it is navigated from.
  3. popEnterAnim specifies how the Notes screen will be animated when it is navigated back to (when backstack is popped on the Note Detail screen by using the Up or Back buttons).
  4. popExitAnim specifies how the Note Detail screen will be animated when it is navigated from (when backstack is popped on the Note Detail screen by using the Up or Back buttons)

The navigation-ui dependency comes with a couple of basic built-in animations and, as you see, it is a breeze to add new ones. It’s almost too easy!

The Navigation AAC also simplifies shared element transitions between destinations. Everyone who has dealt with shared element transitions before would know what a pain it is. Including this functionality in the navigation library is a welcome move! This feature can be implemented programmatically only since you need to reference specific views.

XML vs Programmatic Approach

Setting up the navigation graph in XML is only one way to use the new Navigation Arch Component. You can do all the same things programmatically in code. However, I see a huge benefit in being able to model your navigation flows in XML (with or without nested graphs). It makes it easier to see and edit (in or outside of the visual editor) all of the navigation rules including arguments and deep links in one place. It serves as documentation for your project and makes maintaining this code easier in the future. Unlike Storyboard on iOS, the navigation graph can be easily diffed and code reviewed just like a regular layout XML file.

Managing top-level destinations with AppBarConfiguration

You can use AppBarConfiguration to customize your top-level destinations (ones that don’t have an Up button in the top app bar).

To customize which destinations are considered top-level, pass a set of destination IDs to the AppBarConfiguration constructor:

val appBarConfiguration = AppBarConfiguration(setOf(R.id.main, R.id.android))

Or pass a DrawerLayout to the AppBarConfiguration constructor so that the drawer icon is displayed on all top level destinations:

val appBarConfiguration = AppBarConfiguration(navController.graph, drawerLayout)

Bonus: Migrating to AndroidX

Originally this app was targeting api level 27 and used the support library.

Check out this commit for migrating steps to AndroidX. And don’t forget to set the compile SDK version to 29 (Android Q) in the app module settings in the Android Studio.

For a small app like this one, the migration to AndroidX was a piece of Pie which is very encouraging.

Conclusion

I really like the new Navigation Architecture Component. While still in alpha, the library is already mature enough to start using in Production. Google is doing a great job seeking ideas and bug reports from developers and helping out on StackOverflow (just use the right tags: android-architecture-navigation, android-navigation-component). Kudos to Ian Lake in particular.

Having a Single Source of Truth navigation graph to drive the entire app including the deep links is a huge step forward in making development easier and user experience more consistent.

Personally, I would love to be able to test the navigation graph (or nested navigation graphs) with JUnit alone without resorting to Espresso and hope that more testing abilities are added in the near future.

Some complain that the library does not give you enough control over the backstack but I am yet to run into this issue myself. I like keeping things simple and intuitive — it speeds up both development and maintenance.

I believe that the new library is well on its way to becoming the navigation library on Android.

Resources

  1. Source code for this article can be found at https://github.com/jshvarts/NotesNavigation
  2. Manage UI Navigation with Navigation Controller
  3. https://www.bignerdranch.com/blog/navigation-architecture-components/
  4. https://android-developers.googleblog.com/2019/03/android-jetpack-navigation-stable.html

Visit my Android blog to read about Jetpack Compose and other Android topics

--

--