NavigationUI

Murat Yener
Android Developers
Published in
6 min readApr 14, 2021

--

This is the second MAD Skills article series on Navigation. In this article we’ll take a look at another use case where UI components, such as action bar, bottom tabs or a drawer, are used to navigate between different parts of an app. If you prefer to consume this content in a video format, check it out here:

Introduction

In the previous navigation series, Chet developed an app for tracking donuts. But what goes well with a donut? (besides a second donut): Coffee! So I decided to add functionality to track coffee as well.

I need to add more destinations in the app so it might be a good idea to add a navigation drawer or bottom tabs to help users to navigate. But how do we integrate navigation with these UI components? Through click listeners to manually trigger navigation actions?

No! No listeners required. Instead, NavigationUI class can help you to navigate between different destinations by matching destination and menu ids. Let’s dive in and see how that works.

Adding Coffee Tracker

project structure

To add this feature I copy the donut related classes into a new package and rename them. This might not be the best approach for a real app, but helps us quickly add coffee tracking functionality to the existing app. If you want to follow along, you can check out this repo which contains all the changes on Donut Tracker app to start with NavigationUI.

With these changes I also update the navigation graph with the new destinations and actions from coffeeFragment to coffeeDialogFragment and from selectionFragment to donutFragment. I’ll use these destination ids later ;)

Navigation graph with the new destinations

With the navigation graph updated we can start to tie things together and enable navigation to SelectionFragment!

Options menu

The app currently has an options menu which does … nothing. To make it do something, in the onOptionsItemSelected() function I need to call onNavDestinationSelected() for the selected menu item and pass in the navController. This function will navigate to the destination associated with the given MenuItem as long as destination and MenuItem ids match.

override fun onOptionsItemSelected(item: MenuItem): Boolean {
return item.onNavDestinationSelected(
findNavController(R.id.nav_host_fragment)
) || super.onOptionsItemSelected(item)
}

Now that the navigation controller knows about the menu items, I match the MenuItem ids with the destination ids which I created earlier. With this, navigation is able to map the MenuItems with the destinations.

<menu 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"
tools:context="com.android.samples.donuttracker.MainActivity">
<item
android:id="@+id/selectionFragment"
android:orderInCategory="100"
android:title="@string/action_settings"
app:showAsAction="never" />
</menu>

Toolbar

The app now navigates to selectionFragment but the title stays the same. We would like the title to be updated and show a back button when we are on the selectionFragment.

First I need to add an AppBarConfiguration object which is used by the NavigationUI to manage the behavior of the Navigation button in the upper-left corner of your app.

appBarConfiguration = AppBarConfiguration(navController.graph)

This button changes behavior depending on your destination level. For example, when you are at the Top-level destination, the Up button is not displayed since there is no higher level destination.

By default, the start destination of your app is the only top-level destination but you can define multiple top-level destinations. For example, in our app, I can add both donutList and coffeeList destinations as top level destinations.

Next, I go to the MainActivity class, get an instance of navController, and the toolbar and validate whether the setSupportActionBar() function is called. I also update the toolbar reference passed to this function.

val navHostFragment = supportFragmentManager.findFragmentById(
R.id.nav_host_fragment
) as NavHostFragment
navController = navHostFragment.navController
val toolbar = binding.toolbar

To add navigation support to the default action bar, I call the setupActionBarWithNavController() function. This function expects two parameters, the navController and the appBarConfiguration.

setSupportActionBar(toolbar)
setupActionBarWithNavController(navController, appBarConfiguration)

Next, I override the onSupportNavigationUp() function and call navigateUp() with appBarConfiguration on nav_host_fragment to support up navigation or showing the menu icon, depending on the current destination.

override fun onSupportNavigateUp(): Boolean {
return findNavController(R.id.nav_host_fragment).navigateUp(
appBarConfiguration
)
}

Now I navigate to the selectionFragment, and you can see that the label is updated and the back button is shown, which will let our users go back where they come from.

Title is updated and the back button is displayed

Bottom Tabs

This doesn’t look too bad so far, but the app doesn’t have a way to navigate to the coffeeList fragment. I will fix that next!

I’ll start with adding bottom tabs. To do that I add bottom_nav_menu.xml and declare two menu items. NavigationUI relies on the MenuItem ids to match the destinations ids from the navigation graph. I also set the icons and the title for each destination.

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:id="@id/donutList"
android:icon="@drawable/donut_with_sprinkles"
android:title="@string/donut_name" />
<item
android:id="@id/coffeeList"
android:icon="@drawable/coffee_cup"
android:title="@string/coffee_name" />
</menu>

Now that the MenuItems are ready, I add the BottomNavigationView to the layout of mainActivity and set the bottom_nav_menu which I created earlier as the menu property of the BottomNavigationView.

<com.google.android.material.bottomnavigation.BottomNavigationView
android:id="@+id/bottom_nav_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:menu="@menu/bottom_nav_menu" />

To wire up the bottom tabs, I pass the navController to BottomNavigationView by calling the setupWithNavController() function. To keep things more organized, let’s do that in a new method and call this method in onCreate().

private fun setupBottomNavMenu(navController: NavController) {
val bottomNav = findViewById<BottomNavigationView>(
R.id.bottom_nav_view
)
bottomNav?.setupWithNavController(navController)
}

Notice I didn’t call any navigation action from the navigation graph. Actually the navigation graph doesn’t even have a route to the coffeeList fragment. Like I did earlier with the ActionBar, BottomNavigationView automatically handles the navigation for us by using matching ids of MenuItems and navigation destinations!

Navigation Drawer

This looks great but if your device has a large screen bottom tabs might not offer the best user experience. To fix this I’ll use another layout file which has a w960dp qualifier to target larger/wider devices.

This layout already has a Toolbar and FragmentContainerView similar to the default activity_main layout. I need to add a NavigationView and also add nav_drawer_menu as the menu attribute of the NavigationView. Next, I’ll add a divider between the NavigationView and FragmentContainerView.

<RelativeLayout
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="com.android.samples.donuttracker.MainActivity">
<com.google.android.material.navigation.NavigationView
android:id="@+id/nav_view"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
app:elevation="0dp"
app:menu="@menu/nav_drawer_menu" />
<View
android:layout_width="1dp"
android:layout_height="match_parent"
android:layout_toEndOf="@id/nav_view"
android:background="?android:attr/listDivider" />
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:background="@color/colorPrimary"
android:layout_toEndOf="@id/nav_view"
android:theme="@style/ThemeOverlay.MaterialComponents.Dark.ActionBar" />
<androidx.fragment.app.FragmentContainerView
android:id="@+id/nav_host_fragment"
android:name="androidx.navigation.fragment.NavHostFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_below="@id/toolbar"
app:defaultNavHost="true"
android:layout_toEndOf="@id/nav_view"
app:navGraph="@navigation/nav_graph" />
</RelativeLayout>

With this, the NavigationView is always on screen on wide screen devices instead of the BottomNavigationView. Now that the layout is ready, I create a nav_drawer_menu.xml and add the donutList and coffeeList destinations as a part of the primary group. As the final MenuItem, I add the selectionFragment destination.

<menu xmlns:android="http://schemas.android.com/apk/res/android">
<group android:id="@+id/primary">
<item
android:id="@id/donutList"
android:icon="@drawable/donut_with_sprinkles"
android:title="@string/donut_name" />
<item
android:id="@id/coffeeList"
android:icon="@drawable/coffee_cup"
android:title="@string/coffee_name" />
</group>
<item
android:id="@+id/selectionFragment"
android:title="@string/action_settings" />
</menu>

Now that the layouts are ready let’s switch to MainActivity and set up the drawer to work with the NavigationController. Similar to what I did for BottomNavigationView, I create a new method and pass the navController to NavigationView by calling the setupWithNavController() function. To keep things more organized, let’s do that in a new method and call this method in onCreate().

private fun setupNavigationMenu(navController: NavController){
val sideNavView = findViewById<NavigationView>(R.id.nav_view)
sideNavView?.setupWithNavController(navController)
}

Now when I run the app on a wide screen device, I see that the navigation drawer is set up with the MenuItems, since the MenuItem ids match the destination ids in the navigation graph.

Donut Tracker on wide screen device

Notice how the back arrow is automatically displayed on top left as I navigate. If you want to, you can change the AppBarConfiguration to include the CoffeeList as a top level destination as well.

Summary

That is it! Well at least for this time. Donut Tracker app didn’t need bottom tabs or a navigation drawer but with the new functionality and destinations, NavigationUI helped us greatly to organize the navigation in the app.

We didn’t really need to do much, except for adding the UI components and matching the MenuItem and destinations ids. You can check out the complete code and compare the changes with the stater code.

--

--