NavigationUI
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
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 ;)
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
id
s 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
id
s with the destination id
s which I created earlier. With this, navigation is able to map the MenuItem
s 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
andcoffeeList
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 NavHostFragmentnavController = 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.
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
id
s to match the destinations id
s 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 MenuItem
s 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 id
s of MenuItem
s 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 MenuItem
s, since the MenuItem
id
s match the destination id
s in the navigation graph.
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 id
s. You can check out the complete code and compare the changes with the stater code.