Jetpack Compose Destinations

In this tutorial, you’ll learn how to implement an effective navigation pattern with Jetpack Compose, in a way that will work with different screen sizes, from phones to tablets. By Roberto Orgiu.

Leave a rating/review
Download materials
Save for later
Share

Compose is in its early stages and navigation is one of the most complex topics.
With Compose Destinations you have a central API for navigating between screens and taking care of the back stack. In this tutorial, you’ll use Compose Destinations to implement navigation in an app called Landscapes. Say hello to Jetpack Compose Destinations!

Here’s a summary of the various lessons you’ll learn from this tutorial:

  1. How to implement routes and destinations
  2. How to implement navigation using the Bottom Navigation UI pattern
  3. Animate when navigating between different screens
  4. Handle layouts based on screen dimensions

Getting Started

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

You’re going to build an app that will show pictures of several cities all over the world in a classic list/detail pattern. You’ll also implement the navigation for a Settings screen, just to make things a bit more challenging.

On a phone, you’ll put navigation at the bottom to let the user switch between the list of the cities and the settings. Tapping any of the navigation items will bring the user to the related screen, and tapping an item in the list of cities will hide the bottom navigation bar and show some pictures related to each city. Tapping the back button will bring the user back to the list of cities.

On a tablet, though, the navigation will be different. You’ll implement the list/detail pattern by putting the two screens side by side, with a lateral navigation rail to switch from this content to the settings.

Note: Remember that when you deal with different screen sizes, you might encounter solutions that could look strange. If that happens, don’t worry — it’s normal. Take a step back and remember that everything you’re learning will need to work on several screen sizes.

Import the project into Android Studio. Once it finishes downloading all the dependencies, build and run. You’ll see… nothing! An empty screen will welcome you, and it’s time to start.

Getting started

Setting Up the Project

Open build.gradle of your app module, and you’ll find this dependency:

implementation("androidx.navigation:navigation-compose:2.4.2")

This is the dependency you’ll need to implement the navigation. In this project, you won’t need to declare it and you won’t need to poke around the screens. You can if you want to, but in this tutorial you’ll focus on the navigation.

Now that you have all necessary dependencies, you can start coding!

Note: When you read destination, it means the screens that show to your users, while routes are the paths the user needs to walk to reach the content.

Adding the First Destination

Open CompatUi.kt. In CompatUi(), look for items. Notice that the value is a list of screens available for your users. You’ll start by showing the list of the cities.

Add this code inside the Scaffold() at // TODO: Define NavHost here:

NavHost(
      navController = navController,
      startDestination = Screen.List.path,
      modifier = Modifier.padding(innerPadding)
    ) {
      composable(Screen.List.path) {
        CityListUi(viewModel = viewModel,
          onCitySelected = { /* Replace here */ })
      }
    }

With this code, you create a NavHost — the shell that contains all your destinations. You instruct it to show a specific startDestination, and you define how the destination should display by adding the right Composable to it.

Now, open MainUi.kt, and add this line of code right above the theme declaration:

val navController = rememberNavController()

You’re asking Jetpack Compose Navigation Library to give you the NavController — you’ll need it to create the NavHost.

Now, add this line at the end of // TODO: Add CompatUi() here:

CompatUi(navController = navController, viewModel = viewModel, themeStore = themeStore)

This line simply loads the UI you just created when the app starts. You’re using navController to present a screen — viewModel contains logic for the screen parts and themeStore defines an appropriate theme.

Build and run. Notice the app shows the list of cities. Scroll down to see all cities you used.

First destination

You’re done with the first step! You just added your first destination. Next, you’ll add a couple more destinations, plus a way to reach them.

Navigating to the Detail screen

In this section, you’ll add the city detail and the settings screen. Don’t worry — the screens are ready for you to use in your code. All you have to do is link them so that when a user taps a city, it opens a screen that shows more pictures of that city.

In CompatUi.kt, find the /* Replace here */ comment and replace it with the click behavior:

navController.navigate("detail/${it.name}")

This line instructs the NavController where to go when the user taps a city. In this case, the app will navigate to a screen with details of a tapped city.

Now, you need to add more destinations! Below the lambda of composable(Screen.List.path), add the following code:

// 1
composable(route = "detail/{city}") { backstackEntry ->
    val cityName = backstackEntry.arguments?.getString("city") ?: error("City is required")
    val city = viewModel.cities.first { it.name == cityName }
    CityDetailUi(viewModel = viewModel, city = city, isBigLayout = false)
    }
// 2    
composable(route = Screen.Settings.path) {
    SettingsUi(themeStore = themeStore)
}

There are two different parts:

  1. You define a destination that users reach by tapping a city using composable(). Here, you get the backstackEntry, from which you can extract all destination arguments (in this case, the name of the city). Take a look at the route definition, and you’ll notice city appearing inside curly braces. This is how Jetpack Compose Navigation indicates the variable data (arguments) you can pass to the different routes. Once you have the argument, you can pass it to CityDetailUi().
  2. You specify the settings screen by passing themeStore. You can’t reach that screen yet, but soon you’ll implement all you need to be able to see it.

Build and run the app. When it starts, tap a city to see its details:

Details screen

Implementing Bottom Navigation

As you’re fully aware, you can’t reach the settings screen just yet. But this changes right now!

The next step is to add a BottomNavigation that interacts with your shiny NavHost and lets you reach the settings screen as well.

In order to do it, continue working in CompatUi.kt, but this time inside Scaffold().

At // TODO: Define bottomBar here, paste the following code:

bottomBar = {
      BottomNavigation(backgroundColor = MaterialTheme.colors.primary) {
        // 1
        val navBackStackEntry by navController.currentBackStackEntryAsState()
        val currentDestination = navBackStackEntry?.destination

        // 2
        items.forEach { screen ->
          BottomNavigationItem(
            // 3
            icon = {
              Icon(
                imageVector = screen.icon,
                contentDescription = screen.name
              )
            },
            label = { Text(text = screen.name) },
            // 4
            selected = currentDestination?.hierarchy?.any { it.route == screen.path } == true,
            // 5
            onClick = {
              navController.navigate(screen.path) {
                popUpTo(navController.graph.findStartDestination().id) {
                  saveState = true
                }
                launchSingleTop = true
                restoreState = true
              }
            })
        }
      }
    }

Here’s what the code above does:

  1. Since the BottomNavigation has more than one destination stored inside a stack, you need to know where you are so that you can react accordingly. With this line, you’re getting the exact place where you are in the stack.
  2. You cycle through the different destinations available for the BottomNavigation, and you create a BottomNavigationItem for each of them.
  3. You assign an icon and label for each BottomNavigationItem.
  4. This one is particularly interesting: While looping through all the destinations, you check whether the current destination is presented in the app. When you run into the presented one, you mark it as selected.
  5. Whenever the user taps on the item, you instruct the NavController to reach a specific destination in the stack.

Build and run. You can now reach the settings screen by simply tapping on the Settings tab in the bottom navigation bar!

Settings screen