All videos. All books. One low price.

Get unlimited access to all video courses and books on this site with the new raywenderlich.com Ultimate Subscription. Plans start at just $19.99/month.

Home Android & Kotlin Tutorials

Navigation Component for Android Part 3: Transition and Navigation

In this tutorial, you’ll learn how to use shared element transitions, action bar and bottom navigation to make an app that shows a list of random dogs images.

4.7/5 7 Ratings

Version

  • Kotlin 1.3, Android 5.0, Android Studio 3.6

Navigation is an essential part of Android development. The most common way to do it is via Intents and Fragment transactions. This works great for simple cases but, as the framework evolved, handling navigation became harder with more complex UI designs.

Fragments inside Fragments, deep links, bottom navigation bars, navigation drawers… You probably just felt a shiver down your spine picturing yourself handling a few of these together with just Intents and Fragment transactions.

Jetpack’s Navigation Component is Google’s attempt to earn back the Android developer’s love. Those examples given above? Navigation Component can handle all of them at the same time with a few lines of code.

It’s becoming a must have in any Android developer’s skill set!

In this tutorial, you’ll learn use Navigation Component for:

  • Shared element transitions.
  • Controlling the Action Bar.
  • Handling bottom navigation.
Note: This tutorial assumes you’re familiar with the basics of nav graphs. If you aren’t, please review Navigation Component for Android Part 2: Graphs and Deep Links first.

As you explore Navigation Component, you’ll work on an app named My Little Doggo. It’s an app that shows you a list of random dog images and lets you mark your favorites.

User exploring My Little Doggo, looking at and favoring dogs

OK, time to look at some cute dogs. :]

Getting Started

Download the project materials by clicking the Download Materials button at the top or bottom of this tutorial.

Launch Android Studio 3.6 or later and select Open an existing Android Studio project. Then navigate to and select the starter project folder. You’ll see a structure like this:

app structure

Explore the project for a bit. Focus on the presentation package, where all the UI related code resides.

Then, go to the res package. You’ll see there’s already a package called navigation with three different nav graphs. The main nav graph is app.xml. Its only purpose is to nest the other two, doggo_list and favorites.xml, which you’ll work with later in this tutorial.

Note: While the app uses Room, Coroutines and Flow, you don’t need prior knowledge of any of these to complete the tutorial. However, if you want to learn about, checkout Android Jetpack Architecture Components: Getting Started for Room and Kotlin Coroutines Tutorial for Android: Getting Started.

Build and run the project. You’ll see a simple screen with a list of doggos. The app already uses a simple implementation of Navigation Component that lets you click a doggo to see it in full screen.

User clicks a dog image form list, image shows in full screen

This is a great app to show to any UX designer, but it has some problems:

  • Action Bar doesn’t update.
  • Full screen view just snaps in.
  • Button can’t navigate to your favorites.

It’ll take some work, but the doggos will keep you company every step of the way. :]

Fetching Dependencies

To kick things off, you need to add some dependencies:

  • Material Components: Needed for transitions and bottom navigation.
  • Navigation UI: Responseable for handling the action bar, bottom navigation and navigation drawer.

Head to the app build.gradle. At the bottom, right below the navigation dependency, add these two lines:

implementation "androidx.navigation:navigation-ui-ktx:$nav_version"
implementation "com.google.android.material:material:$material_version"

The versions are already in the root build.gradle. Sync Gradle and run the app to make sure everything is OK.

Sharing Elements Between Screens

Shared element transitions are a neat way to express continuity and flow smoothly between screens. You can use them to help the user better understand the flow of information when you have common elements between screens.

Many Doggos and One Doggo screens share common element of Border Collie image

To use shared element transitions, you need to define which views transition between screens and give each of those transitions a unique name.

Since the app uses Fragments to navigate, you have to tinker with Fragment transitions by postponing them and starting them later at the right time so they don’t interfere.

Adapting the Adapter

Expand presentation and open DoggosAdapter.kt.

As mentioned before, each shared element transition needs a unique name. In this case, the picture URL is a good option. In bind() method, add the following line below load(item.picture){...} code block:

transitionName = item.picture

This sets the transitionName to the corresponding Doggo picture url.

That’s all for the Adapter.

Well done! Build and run the app to make sure you didn’t break anything. The app will launch and behave as before.

You won’t see any differences in the animation yet. You still need to tell Navigation Component which views it should transition.

Adding Shared Elements to NavController

Now, click the doggos package and open DoggoListFragment.kt.

First, add a helper method right at the bottom of the class, which will be used to simplify navigation:

private fun navigate(destination: NavDirections, extraInfo: FragmentNavigator.Extras) = with(findNavController()) {
    // 1
    currentDestination?.getAction(destination.actionId)
        ?.let { navigate(destination, extraInfo) //2 }
  }

Here is what is happening in this function:

  1. Make sure the current destination exists, by checking if the passed destination’s action id resolves to an action on the current destination. This avoids attempting to navigate on non-existent destinations
  2. Passing FragmentNavigatorExtras instance with the extra information into NavController through its navigate method.

Now to use it, navigate to method called createAdapter(). This method creates the Adapter for the RecyclerView and is where you pass a lambda to DoggosAdapter.

Delete the findNavController().navigate(toDoggoFragment) line and add the following in its place:

//1
val extraInfoForSharedElement = FragmentNavigatorExtras(
  //2
  view to doggo.picture
)
//3
navigate(toDoggoFragment, extraInfoForSharedElement)

Here’s a code breakdown:

  1. FragmentNavigatorExtras is the class you use to tell Navigation Component which views you want to share, along with the corresponding transition name, which you previously set as the picture URL.
  2. Writing view to doggo.picture is the same as writing Pair(view, doggo.picture).
  3. In this code, you pass the created FragmentNavigatorExtras instance with the transition information into the helper method created earlier navigate method.

You’re halfway done!

Build and run the app. You still won’t see any difference in the animation, but the app would run and behave like before.

Next, you’ll tell the destination Fragment that there’s an element to transition.

Walking the Doggo to Another View

You need to define which kind of transition you want the Fragment to do. So, you’ll create a transition and set it to the Fragment’s sharedElementEnterTransition property.

First, right-click the res package and select New ▸ Android Resource Directory. On the Resource type dropdown, select transition. It’ll automatically change the directory name:

New Resource Directory with resource as transition and directory name as transition

Now, click OK. You’ll see a transition directory inside res.

Res package with transition directory boxed in red

Next, right-click transition and select New ▸ Transition resource file. Call it shared_element_transition and click OK. Then delete everything inside and paste the following:

<?xml version="1.0" encoding="utf-8"?>
<changeBounds xmlns:android="http://schemas.android.com/apk/res/android"
    android:interpolator="@android:interpolator/fast_out_slow_in" />
Note: There are a few types of transitions, each with different properties. You can even group them together using something called a TransitionSet. Read more about these in the official documentation.

You have your transition defined. Now, hand it to the destination Fragment.

First, expand the doggodetail package and open DoggoFragment.kt. Looking at the code, you can tell DoggoFragment already uses Navigation Component.

It fetches the doggo’s picture URL and favorite status through navArgs() and uses them to set up its UI.

Now, at the bottom of DoggoFragment, add the following method:

private fun setSharedElementTransitionOnEnter() {
  sharedElementEnterTransition = TransitionInflater.from(context)
      .inflateTransition(R.transition.shared_element_transition)
}
Note: When Android Studio asks you to resolve the dependency for TransitionInflater, be sure to import TransitionInflater from androidx.transition and not android.transition.

The app will crash if you import the wrong dependency! If androidx.transition doesn’t appear, it means you didn’t import Material Components. Go to the Fetching Dependencies section of the tutorial to see how it’s done.

You’re setting sharedElementEnterTransition in the method, but you’re not calling it anywhere. That won’t do.

Fix this by calling setSharedElementTransitionOnEnter() and postponeEnterTransition() in onViewCreated():

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
  super.onViewCreated(view, savedInstanceState)
  val (picture, isFavorite) = args

  // Add these two lines below
  setSharedElementTransitionOnEnter()
  postponeEnterTransition()

  setupFavoriteButton(picture, isFavorite)
  image_view_full_screen_doggo.load(picture)
}

You already know what setSharedElementTransitionOnEnter() is for, but what’s with postponeEnterTransition()?

Well, the images you want to transition are loaded into the Fragment view by Glide. Loading images takes time, which means that the view runs its transitions before the image is available, messing up the animation. The trick here is to postpone the Fragment enter transition and resume it after the images finish loading.

Pretty clever, huh?

Note: If you’ve never used Glide before, be sure to check Glide Tutorial for Android: Getting Started.

To do this, you’re going to take advantage of the fact that Glide lets you add request listeners to its image loading.

First, add the following method at the bottom of DoggoFragment, right below setSharedElementTransitionOnEnter():

private fun startEnterTransitionAfterLoadingImage(
    imageAddress: String, 
    imageView: ImageView
) {
  Glide.with(this)
      .load(imageAddress)
      .dontAnimate() // 1
      .listener(object : RequestListener<Drawable> { // 2
        override fun onLoadFailed(
            e: GlideException?,
            model: Any?,
            target: com.bumptech.glide.request.target.Target<Drawable>?,
            isFirstResource: Boolean
        ): Boolean {
          startPostponedEnterTransition() 
          return false
        }

        override fun onResourceReady(
            resource: Drawable,
            model: Any,
            target: com.bumptech.glide.request.target.Target<Drawable>,
            dataSource: DataSource,
            isFirstResource: Boolean
        ): Boolean {
          startPostponedEnterTransition() 
          return false
        }
      })
      .into(imageView)
}

Now, resolve all the reference errors. When multiple imports are possible, be sure to pick the ones prefixed with com.bumptech.glide.

This code is the basic Glide usage with two extra calls:

  1. You don’t want Glide to mess up things with its default crossfade animation. As such, you call dontAnimate() to avoid it.
  2. RequestListener needs you to override onLoadFailed and onResourceReady. You call startPostponedEnterTransition() in both of them because you need to even if the request fails. If you don’t call it, the UI will freeze after calling postponeEnterTransition().

Your transition is almost ready!

Now, go back to onViewCreated() in DoggoFragment.kt and replace image_view_full_screen_doggo.load(picture) with:

image_view_full_screen_doggo.apply {
  //1
  transitionName = picture
  //2
  startEnterTransitionAfterLoadingImage(picture, this)
}

Here you:

  1. Let the image know it has a transition by setting its transitionName.
  2. Call the method to load the picture into the ImageView and resume the enter transition after the load finishes.

The shared element transition is now complete. Whew!

Build and run, then give the app a whirl. Congrats on your cool transitions!

User selects a doggo, doggo goes fullscreen, user selects back button and returns to list with no transition

Wait, what? Where’s the return transition? Did a doggo run away with it?

Teaching the RecyclerView to Stay

No, a doggo didn’t run away with your transition. They’re well-trained!

You’re facing a problem you had before, but with a different component. The dog pictures displayed by the RecyclerView also need to be loaded. This load takes more time than the RecyclerView needs to set up everything else.

The fix? Same as before. Call postponeEnterTransition() followed by startPostponedEnterTransition(). The difference is, this time you’ll do it with the RecyclerView.

First, go back to DoggoListFragment.kt. At the end of the setupRecyclerView(), right below addOnScrollListener, add the following code:

//1
postponeEnterTransition()
//2
viewTreeObserver.addOnPreDrawListener {
  //3
  startPostponedEnterTransition()
  true
}

There are three new elements here:

  1. You postpone the Fragment’s enter transition.
  2. Then you set a listener onPreDraw to the RecyclerView. This callback is only invoked when all the views in the RecyclerView view tree are measured.
  3. You call startPostponedEnterTransition() as soon as all the images finish loading.

Build and run the app again. Don’t you love it when things work correctly?

User selecting doggos with correct tranisitions

Notice that the other images appear and disappear without any animation at all.

You can solve this by going to res ▸ navigation ▸ doggo_list.xml and replacing the action tag of doggoListFragment that navigates to DoggoFragment with below:

<action
  android:id="@+id/to_doggoFragment"
  app:destination="@id/doggoFragment"
  app:enterAnim="@anim/fragment_fade_enter"
  app:exitAnim="@anim/fragment_fade_exit"
  app:popEnterAnim="@anim/fragment_fade_enter"
  app:popExitAnim="@anim/fragment_fade_exit" />

Build and run the app. Poof! No more animation glitches!

This was the most complex part of the tutorial. Congrats on making it this far!

Controlling the Action Bar

The Action Bar is one of the most important design elements in Android. It not only provides consistency between apps but also allows users to quickly interact with a familiar set of elements.

Navigation Component provides default support for Action Bars through the NavigationUI class. The Action Bar in this app is the theme’s default.

Navigation Component also guarantees the principles of navigation for the Action Bar are followed:

  • Up and Back are identical within your app task.
  • The Up button never exits your app.

In the presentation package, open MainActivity.kt. At the top of the class, right above onCreate, add the following properties:

private val navController by lazy { findNavController(R.id.nav_host_fragment) }
private val appBarConfiguration by lazy { AppBarConfiguration(navController.graph) } 

Now resolve the import about findNavController choosing findNavController(Activity.Int)(androidx...).

These should already be familiar to you from the part 2 of this tutorial. This NavController is the same one used to navigate in DoggoListFragment.

Now you need to connect the Action Bar to NavController. Inside the setupActionBar(), add the following one liner:

setupActionBarWithNavController(navController, appBarConfiguration)

Resolve the reference error, then build and run the app.

You’ll see the Action Bar now updates the title and shows the Up button. Success!

User selecting doggos but the up button doesn't work

OK, the Up button doesn’t work. Deep inside, you knew it was too good to be true. :]

There’s one last step. Now you need to override onSupportNavigateUp so NavController will handle clicks on the Navigation button. Add the following method above the setupActionBar() method declaration:

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

If you were using your own Toolbar instead of the default Action Bar, you wouldn’t need to override onSupportNavigateUp(). With Toolbar, Navigation automatically handles click events for the Navigation button.

Build and run the app. You now have a working Up button!

User selects doggos and uses up button to return to list

Adding a Menu Item

Sometimes, you want certain screens to show menu items in the Action Bar. With Navigation Component, it only takes a few lines of code.

There’s already an About Fragment in the app’s nav graph, so you’ll add a menu item that navigates to that Fragment.

First, you need to create the menu item.

Right-click your res package and select New ▸ Android Resource File. Then, on the Resource type dropdown, choose menu. Call it menu_about. Delete everything in it and paste the following:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
    android:id="@+id/aboutFragment"
    android:title="@string/about"
    android:icon="@drawable/ic_settings_24dp"
    app:showAsAction="always"/>
</menu>

Just your typical menu item. However, take a look at the item’s ID.

If you open the doggo_list.xml nav graph, you’ll notice that AboutFragment has the same ID. This isn’t a coincidence. These IDs must match for the navigation to work.

Boxed fragment and item IDs showing they match

Now, open DoggoFragment.kt. You’ll add the menu item here. In onCreateView(), right before the return, add this line:

setHasOptionsMenu(true)

This tells the system that the Action Bar in this Fragment should display a menu item.

Next, tell the system which menu item to display and what to do with it. Paste this code after onCreateView():

// 1
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
   inflater.inflate(R.menu.menu_about, menu)
}
// 2
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return item.onNavDestinationSelected(findNavController()) ||
                super.onOptionsItemSelected(item)
}

Resolve the import errors. In these method overrides, you:

  1. Inflate the menu item.
  2. Call onNavDestinationSelected, which is a NavigationUI helper method. This method takes in the NavController and, if the IDs of the destination and the menu item match, uses it to navigate to that destination.

Build and run the app. Try your new button:

User selects a dog, selects button and is taken to start screen

Notice anything strange? As soon as you press the Up button, you’re back to the start destination. The back stack is effectively popping back to the nav graph start destination.

You can fix this by adding android:menuCategory="secondary" to your menu item inside the menu_about.xml. Add it below android:id:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
    android:id="@+id/aboutFragment"
    android:menuCategory="secondary"
    android:title="@string/about"
    android:icon="@drawable/ic_settings_24dp"
    app:showAsAction="always"/>
</menu>

This way, onNavDestinationSelected() knows it shouldn’t pop the whole back stack.

Build your app, and try it out.

App correctly works

Look at you go! All that’s missing now is a Bottom Navigation setup inside the app.

Implementing Bottom Navigation

Each button of a bottom navigation bar represents a top-level destination. You should only use bottom navigation when you have three to five top-level destinations of equal importance. This app only has two, but, after all, it’s a demo.

The way you represent destinations varies with their number:

  • Three destinations: Display icons and text labels for all.
  • Four destinations: Active destinations display an icon and text label. Inactive destinations display icons and text labels are recommended.
  • Five destinations: Active destinations display an icon and text label. Inactive destinations use icons and use text labels if space permits.
Note: Icons are always mandatory but text labels are optional. If you want text, keep it short. Otherwise, you’ll face the Material Design Police for truncating or wrapping text in a bottom nav bar.

Updating Styles and Layouts

Now you’ll use the BottomNavigationView from Material Components. In the app, navigate to res ▸ values ▸ styles.xml. Update your themes to use Material Components.

If you don’t do this, the bottom navigation bar won’t display correctly.

Simply replace AppCompat with MaterialComponents i.e Theme.AppCompat.DayNight.DarkActionBar is replaced by Theme.MaterialComponents.DayNight.DarkActionBar. Once done your styles.xml would be like below:

<resources>

  <!-- Base application Theme -->
  <style name="AppTheme" parent="Theme.MaterialComponents.DayNight.DarkActionBar">
    <!-- Customize your theme here. -->
    <item name="colorPrimary">@color/colorPrimary</item>
    <item name="colorPrimaryDark">@color/colorPrimaryDark</item>
    <item name="colorAccent">@color/colorAccent</item>
  </style>

  <!-- Splash Screen Theme -->
  <style name="SplashTheme" parent="Theme.MaterialComponents.NoActionBar">
    <item name="android:windowBackground">@drawable/splash_background</item>
    <item name="android:windowTranslucentStatus">true</item>
    <item name="android:windowTranslucentNavigation">true</item>
  </style>

</resources>

To set the destination buttons, you’ll use a menu. Right-click on the menu package, select New ▸ Menu resource file. Name it menu_bottom_nav. Delete everything inside and add this menu:

<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
  <item
    android:id="@+id/doggoList"
    android:icon="@drawable/ic_list_white_24dp"
    android:title="@string/doggos"
    app:showAsAction="ifRoom" />
  <item
    android:id="@+id/favorites"
    android:icon="@drawable/ic_favorite_white_24dp"
    android:title="@string/favorites"
    app:showAsAction="ifRoom" />
</menu>

The first item is for the doggo list. The second item is for the favorites list, which you couldn’t access until now. As with the Action Bar menu item, the IDs of these items must match the IDs of the actual nav graphs.

Now that you have the menu, you need to add a BottomNavigationView to the layout where the NavHostFragment is.

First, navigate to res ▸ layout and open activity_main.xml. Add the navigation view at the end of the LinearLayout:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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:id="@+id/main_container"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:orientation="vertical"
  tools:context=".presentation.MainActivity">

  <fragment
    android:id="@+id/nav_host_fragment"
    android:name="androidx.navigation.fragment.NavHostFragment"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_weight="1"
    app:defaultNavHost="true"
    app:navGraph="@navigation/app" />

  <!--    Bottom Navigation Component added -->
  <com.google.android.material.bottomnavigation.BottomNavigationView
    android:id="@+id/bottom_navigation"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:menu="@menu/menu_bottom_nav" />
</LinearLayout>

You should already see it in the layout preview. Build and run the app to see the real deal!

Many Doggos screen with bottom navigation buttons

Cute, but the buttons don’t do anything. You still need to connect the BottomNavigationView to the Navigation Component.

Wiring the NavController to Bottom Navigation

First, open MainActivity.kt. Add this line in setupBottomNavigationBar():

bottom_navigation.setupWithNavController(navController)

Resolve any import errors. You’re accessing the bottom nav through bottom_navigation, a synthetic property created by Kotlin through the ID of the BottomNavigationView in the layout.

Build and run the app. The buttons now work correctly.

User successfully clicking favorites and doggos

Not only do the buttons work, but since you’re already controlling the Action Bar with the Navigation Component, the name of the screen updates when you click the destinations.

They go great together! However, you probably noticed the Up button appears in the Action Bar when you click Favorites. That shouldn’t happen in top-level destinations.

Fortunately, there’s a way to fix it.

Fixing The Action Bar Navigation Button

The problem is that Navigation Component doesn’t know which destinations, other than the starting one, are top-level destinations.

The AppBarConfiguration class provides a simple solution. While still in MainActivity.kt, change appBarConfiguration initialization to:

private val appBarConfiguration by lazy { 
  AppBarConfiguration(
      topLevelDestinationIds = setOf(
          R.id.doggoListFragment, 
          R.id.favoritesFragment
      )
  ) 
}

Here, you specify exactly which destinations should be considered top-level. You must pass in the IDs of the Fragments you want to treat as top-level destinations.

Build and run. That’s it!

User testing app with all buttons working correctly

The Multiple Back Stacks Problem

There’s one more thing before you go. Notice the back stack of a top-level destination is destroyed when you go to the other one:

Backstack state not preserved when navigating between top-level destinations

This is a known issue. Navigation Component doesn’t support multiple back stacks yet.

While efforts to support the feature are in progress, it might take a while as it involves changing code at the Fragment level.

Where to Go From Here?

Impressive! You made it to the end with no doggo bites, and, best of all, now you can use Navigation Component to leverage some pretty neat navigation patterns.

Download the completed final version of the project by clicking the Download Materials button at the top or bottom of this tutorial.

For additional examples of Navigation, as well as the navigation principles, check the official documentation.

You can also check Google’s basic and advanced navigation samples.

For bottom navigation guidelines, check the material guidelines.

I hope you enjoyed this tutorial. If you have any questions, tips or want to show off your cool mapping app, feel free to join the discussion below!

Average Rating

4.7/5

Add a rating for this content

7 ratings

More like this

Contributors

Comments