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. By Ricardo Costeira.

Leave a rating/review
Download materials
Save for later
Share

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)
}

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.

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?