“rood signage” by Ernest Brillo on Unsplash

The New Android In-App Navigation

Veronika Figura

--

During the past few weeks I had time to dive into the Navigation Architecture Component that Google presented at this years Google I/O. It is a part of Android Jetpack and its main goal is to ease in-app navigation on Android.

I’m going to show you how navigation component can simplify your everyday Android stuff like navigating when clicking on adapter item or through navigation drawer.

I’ve also created a simple app in Kotlin where you’ll find everything mentioned in this article. So if you have trouble with something, you can look at the whole code at this repo.

☝️ First things first

If you’re completely new to Navigation component, you can go through this post to learn how to create navigation graph with the use of Navigation editor. Or this post series to also help structure your app better. And it’s always good practice to look at the documentation.

There’s also a codelab which is a great way to start if you’re not familiar with this component but you want to learn by coding.

Ready? OK, let’s start by taking a closer look at the Navigation Component.

Navigation Component main players

Before diving into code we should have some knowledge about classes and interfaces that we’re going to work with. Documentation mentions three main components:

  • NavGraph — a collection of destinations. It can be inflated from layout file or created programmatically. You can have multiple navigation graphs in your application and they can be also nested.
  • NavHost — an interface serving as a container that hosts the NavController.
  • NavController — class managing navigation within NavHost by interacting with NavGraph.

Other classes you’ll use include:

NavHostFragment — implementation of NavHost for creating fragment destinations. It has its own NavController and navigation graph. Typically you would have one NavHostFragment per activity.

Navigation — helper class for obtaining NavController instance and for connecting navigation to UI events like a button click.

NavigationUI — class for connecting app navigation patterns like drawer, bottom navigation or actionbar with NavController.

Let’s quickly refresh the basics

Let’s say you’ve created navigation graph XML file and you’ve created a fragment in your activity layout file.

Now you have to set its name to NavHostFragment to indicate that this will be the place for switching destinations while navigating inside this activity. And you have to associate it with an existing navigation graph XML file.

The result should look something like this:

<fragment
android:layout_width=”match_parent”
android:layout_height=”match_parent”
android:id=”@+id/nav_host”
android:name=”androidx.navigation.fragment.NavHostFragment”
app:navGraph=”@navigation/nav_graph”
app:defaultNavHost=”true” //to intercept with system Back button
/>

If you want to see it done programmatically, see the docs.

To navigate between destinations(activities or fragments) you can use:

  • target destination id e.g. navController.navigate(R.id.mainActivity)
  • action id e.g. navController.navigate(R.id.actionDetail)

If you don’t have NavController you can obtain it in multiple ways:

  • in activity by getting NavHostFragment instance:
val host: NavHostFragment = supportFragmentManager
.findFragmentById(R.id.nav_host) as NavHostFragment? ?: return
val navController = host.navController
  • in fragment or view by calling findNavController()
  • or by calling Navigation.findNavController(view) passing a view associated with NavController

To go back to previous destination in graph use navController.navigateUp() method.

To hook up view to navigation use onClickListener:

button.setOnClickListener { view ->
view.findNavController().navigate(R.id.actionDetail)
}

or

button.setOnClickListener(
Navigation.createNavigateOnClickListener(R.id.actionDetail, bundle))

where first parameter is action id or destination id and second is bundle for passing data between destinations. There’s also one argument version of this function when you don’t need to send data.

Returning data to destination

We know how to send data from first destination to second, but what about if we want to get some data back? When working with activities we would typically use startActivityForResult method and get the result in onActivityResult after second activity finishes. I searched if Navigation Component has something similar.

In this issue someone from Google recommended using a shared ViewModel. For activities we should probably continue using the old way, because you can’t share ViewModel between activities like you can with fragments.

Also Google is shifting more towards single activity per app approach and if community follows, getting result back from activity will be less common.

But if you want to share data between fragments, simply create a ViewModel scoped to activity and use it in both fragments. Then you have access to properties in ViewModel from both fragments.

If you need to imitate sending data when navigating back, you can use Event which is a wrapper class for data sent via LiveData. Event was created especially for one time consumable data like displaying SnackBar or Toast message. You can learn more about that in an article from Jose Alcérreca.

Simplifying drawer, ActionBar and bottom navigation

Working with Navigation component saves you not just the boilerplate when writing fragment transactions but it’s very useful also when initializing navigation drawer.

No need to setup ActionBarDrawerToggle and NavigationItemSelectedListener anymore. It turns this pile of code:

val toggle = ActionBarDrawerToggle(this, drawer_layout, null, R.string.navigation_drawer_open, R.string.navigation_drawer_close)
drawer_layout.addDrawerListener(toggle)
toggle.syncState()
toggle.isDrawerIndicatorEnabled = true
nav_view.setNavigationItemSelectedListener { menuItem ->
when (menuItem.itemId) {
R.id.nav_first -> go somewhere…
R.id.nav_second -> go somewhere else…
}
menuItem.isChecked = true
drawer_layout.closeDrawer(Gravity.START)
true
}

Into this:

NavigationUI.setupWithNavController(nav_view, navController)NavigationUI.setupActionBarWithNavController(this, navController, drawer_layout)

Awesome, right?! 👍

For this to work, you need to match item ids in your drawer menu with destination ids in your navigation graph. So if you have fragment with id @+id/home_fragment, you need to use the same id for the corresponding menu item in drawer.

The second line in above code snippet sets hamburger menu icon in actionbar and connects it to drawer. Last thing to do is to override onSupportNavigateUp method so that click on the icon opens the drawer:

override fun onSupportNavigateUp() = NavigationUI.navigateUp(drawer_layout, navController)

If you want to setup only actionbar use the same method with only two arguments:

setSupportActionBar(toolbar)
NavigationUI.setupActionBarWithNavController(this, navController)

If you use bottom navigation instead, just call:

NavigationUI.setupWithNavController(bottom_nav, navController)

Navigating from adapter item

Navigation component makes it easier to react to click events on items in RecyclerView. The old way would be to create a listener interface that the activity/fragment implements and pass its instance to adapter via constructor. Then pass it to ViewHolder so you can call the listener’s method inside item’s on click listener implementation.

With Navigation component you don’t need to create an interface nor pass a listener instance to adapter if you’re only interested in navigating to another screen. Just set onClickListener on item’s view as follows:

view.setOnClickListener(
Navigation.createNavigateOnClickListener(destination_or_action_id)
)

If you want to also send some data, use two argument version of this method and pass data using safe args plugin (more about it here).

val actionDetail = HomeFragmentDirections.ActionDetail()
actionDetail.setTitle(item)
view.setOnClickListener {
Navigation.createNavigateOnClickListener(R.id.actionDetail, actionDetail.arguments)
}

Conditional navigation

Maria Neumayer described this well in her article. To sum it up, this kind of navigation is useful when you have some condition that has to be met to navigate to target destination. Good example is a login screen.

Let’s say user wants to navigate from Home screen to Profile but hasn’t logged in yet. Instead of Profile, Login screen is shown. After a successful login user is taken to the Profile screen. If he navigates back, he should be taken straight to the Home screen. Documentation suggests that

The Login destination should pop itself off the navigation stack after it returns to the Profile destination. Call the popBackStack() method when navigating back to the original destination.

In case your login flow consists from multiple screens, you can use:

findNavController().popBackStack(R.id.premium_flow, true)

Above example will pop all destinations in stack that came after the one specified in 1st parameter. Second parameter describes if the specified destination should be also popped from the stack.

If you’re implementing login flow using a separate activity, simply call activity.finish() before navigating from login to another activity.

This way the login activity won’t stay in the stack and navigating up won’t show it. This is useful also with onboarding flow at app launch, because you don’t want to let user return to it by pressing the back button.

Common destinations and global actions

Common destination is a screen to which can user navigate from multiple parts of the app. For such cases we can use global actions. Unlike normal actions, they are defined on the same level as destinations — inside the root element of graph called navigation. They have an id and target destination. Global action can be used to navigate to its target destination from any other destination in the same navigation graph (nested graph also).

Note: When you define a global action you also have to provide android:id to the navigation element.

<navigation android:id="@+id/main" ...><action android:id="@+id/action_global"
app:destination="@id/detailFragment"/>
<fragment
android:id="@+id/detailFragment"
.../>
</navigation>

Nested navigation graphs

As your app becomes more filled with features you may consider putting some destinations from your navigation graph to a nested graph. It’s basically a graph within your main/root graph. This is useful if you have a setup flow that consists from multiple screens or an onboarding flow.

Creating a nested graph is quite simple and can make your root graph look less complicated and easier to understand.

Also it enables you to reuse the nested flow in your app. That’s because a nested graph has an id which is used as the only access point for navigation from the root graph. You won’t be able to navigate to specific destination in nested graph from the root graph.

<navigation android:id="@+id/main" ...><navigation android:id="@+id/premium_flow"
app:startDestination="@id/premiumStep1Fragment">

<fragment
android:id="@+id/premiumStep1Fragment" .../>
<fragment
android:id="@+id/premiumStep2Fragment" .../>
</navigation>
</navigation>

As you can see, Navigation Component has a lot to offer. It enables us to write less boilerplate code when implementing things like Navigation Drawer and simplifies fragment transactions. It surely is a nice contribution to the Architecture Components family.

There are still some topics that I haven’t covered in this article. Deep linking is well described in this article. Navigation Component testing isn’t really covered in docs yet, but Maria Neumayer covers it briefly here.

If you have any suggestions or questions, feel free to leave a comment below. Don’t forget to leave 👏 if you liked the post. It will be appreciated. ❤️

--

--