Google is committed to advancing racial equity for Black communities. See how.

Navigating with Compose

The Navigation component provides support for Jetpack Compose applications. You can navigate between composables while taking advantage of the Navigation component’s infrastructure and features.

Setup

To support Compose, use the following dependency in your app module’s build.gradle file:

dependencies {
    def nav_compose_version = "1.0.0-alpha01"
    implementation "androidx.navigation:navigation-compose:$nav_compose_version"
}

Getting started

The NavController is the central API for the Navigation component. It is stateful and keeps track of the back stack of composables that make up the screens in your app and the state of each screen.

You can create a NavController by using the rememberNavController() method in your composable:

val navController = rememberNavController()

You should create the NavController in the place in your composable hierarchy where all composables that need to reference it have access to it. This follows the principles of state hoisting and allows you to use the NavController and the state it provides via currentBackStackEntryAsState() to be used as the source of truth for updating composables outside of your screens. See Integration with the bottom nav bar for an example of this functionality.

Creating a NavHost

Each NavController must be associated with a single NavHost composable. The NavHost links the NavController with a navigation graph that specifies the composable destinations that you should be able to navigate between. As you navigate between composables, the content of the NavHost is automatically recomposed. Each composable destination in your navigation graph is associated with a route.

Creating the NavHost requires the NavController previously created via rememberNavController() and the route of the starting destination of your graph. NavHost creation uses the lambda syntax from the Navigation Kotlin DSL to construct your navigation graph. You can add to your navigation structure by using the composable() method. This method requires that you provide a route and the composable that should be linked to the destination:

NavHost(navController, startDestination = "profile") {
    composable("profile") { Profile(...) }
    composable("friendslist") { FriendsList(...) }
    ...
}

To navigate to a composable destination in the navigation graph, you must use the navigate() method. navigate() takes a single String parameter that represents the destination’s route. To navigate from a composable within the navigation graph, call navigate():

fun Profile(navController: NavController) {
    ...
    Button(onClick = { navController.navigate("friends") }) {
        Text(text = "Navigate next")
    }
    ...
}

You should only call navigate() as part of a callback and not as part of your composable itself, to avoid calling navigate() on every recomposition.

Navigation compose also supports passing arguments between composable destinations. In order to do this, you need to add argument placeholders to your route, similar to how you add arguments to a deep link when using the base navigation library:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable("profile/{userId}") {...}
}

By default, all arguments are parsed as strings. You can specify another type by using the arguments parameter to set a type:

NavHost(startDestination = "profile/{userId}") {
    ...
    composable(
        "profile/{userId}",
        arguments = listOf(navArgument("userId") { type = NavType.StringType })
    ) {...}
}

You should extract the NavArguments from the NavBackStackEntry that is available in the lambda of the composable() function.

composable("profile/{userId}") { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

To pass the argument to the destination, you need to add the value to the route in place of the placeholder in the call to navigate:

navController.navigate("profile/user1234")

For a list of supported types, see Pass data between destinations.

Adding optional arguments

Navigation Compose also supports optional navigation arguments. Optional arguments differ from required arguments in two ways:

  • They must be included using query parameter syntax ("?argName={argName}")
  • They must have a defaultValue set, or have nullability = true (which implicitly sets the default value to null)

This means that all optional arguments must be explicitly added to the composable() function as a list:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Now, even if there is no argument passed to the destination, the defaultValue of "me" will be used instead.

The structure of handling the arguments through the routes means that your composables remain completely independent of Navigation and are much more testable.

Navigation Compose supports implicit deep links that can be defined as part of the composable() function as well. Add them as a list using navDeepLink():

val uri = "https://example.com"

composable(
    "profile?id={id}",
    deepLinks = listOf(navDeepLink { uriPattern = "$uri/{id}" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("id"))
}

These deep links let you associate a specific URL, action, and/or mime type with a composable. By default, these deep links are not exposed to external apps. To make these deep links externally available you must add the appropriate <intent-filter> elements to your app’s manifest.xml file. To enable the deep link above, you should add the following inside of the <activity> element of the manifest:

<activity …>
  <intent-filter>
    ...
    <data android:scheme="https" android:host="www.example.com" />
  </intent-filter>
</activity>

Navigation will automatically deep link into that composable when the deep link is triggered by another app.

These same deep links can also be used to build a PendingIntent with the appropriate deep link from a composable:

val id = "exampleId"
val context = AmbientContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://example.com/$id".toUri(),
    context,
    MyActivity::class.java
)

val deepLinkPendingIntent: PendingIntent? = TaskStackBuilder.create(context).run {
    addNextIntentWithParentStack(deepLinkIntent)
    getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}

You can then use this deepLinkPendingIntent like any other PendingIntent to open your app at the deep link destination.

Integration with the bottom nav bar

By defining the NavController at a higher level in your composable hierarchy, you can connect Navigation with other components such as the BottomNavBar. Doing this allows you to navigate by selecting the icons in the bottom bar.

To link the items in a bottom navigation bar to routes in your navigation graph, it is recommended to define a sealed class, such as Screen seen here, that contains the route and string resource id for the destinations.

sealed class Screen(val route: String, @StringRes val resourceId: Int) {
    object Profile : Screen("profile", R.string.profile)
    object FriendsList : Screen("friendslist", R.string.friends_list)
}

Then place those items in a list that can be used by the BottomNavigationItem:

val items = listOf(
   Screen.Profile,
   Screen.FriendsList,
)

In your BottomNavigation composable, get the NavBackStackEntry using the currentBackStackEntryAsState() function, and using the entry, retrieve the route from the arguments by using the KEY_ROUTE constant that is part of NavHostController. Using the route, determine whether the selected item is the current destination and respond appropriately by setting the label, highlighting the item, and navigating if the routes do not match.

val navController = rememberNavController()
Scaffold(
    bottomBar = {
        BottomNavigation {
            val navBackStackEntry by navController.currentBackStackEntryAsState()
            val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
            items.forEach { screen ->
                BottomNavigationItem(
                    icon = { Icon(Icons.Filled.Favorite) },
                    label = { Text(stringResource(screen.resourceId)) },
                    selected = currentRoute == screen.route,
                    onClick = {
                        // This is the equivalent to popUpTo the start destination
                        navController.popBackStack(navController.graph.startDestination, false)

                        // This if check gives us a "singleTop" behavior where we do not create a
                        // second instance of the composable if we are already on that destination
                        if (currentRoute != screen.route) {
                            navController.navigate(screen.route)
                        }
                    }
                )
            }
        }
    }
) {

    NavHost(navController, startDestination = Screen.Profile.route) {
        composable(Screen.Profile.route) { Profile(navController) }
        composable(Screen.FriendsList.route) { FriendsList(navController) }
    }
}

Here you take advantage of the NavController.currentBackStackEntryAsState() method to hoist the navController state out of the NavHost function, and share it with the BottomNavigation component. This means the BottomNavigation automatically has the most up-to-date state.

Testing

We strongly recommended that you decouple the Navigation code from your composable destinations to enable testing each composable in isolation, separate from the NavHost composable.

The level of indirection provided by the composable lambda is what allows you to separate your Navigation code from the composable itself. This works in two directions:

  • Pass only parsed arguments into your composable
  • Pass lambdas that should be triggered by the composable to navigate, rather than the NavController itself.

For example, a Profile composable that takes in a userId as input and allows users to navigate to a friend’s profile page might have the signature of:


@Composable
fun Profile(
    userId: String,
    navigateToFriendProfile: (friendUserId: String) -> Unit
) {
 …
}

Here we see that the Profile composable works independently from Navigation, allowing it to be tested independently. The composable lambda would encapsulate the minimal logic needed to bridge the gap between the Navigation APIs and your composable:

composable(
    "profile?userId={userId}",
    arguments = listOf(navArgument("userId") { defaultValue = "me" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
}

Learn more

To learn more about Jetpack Navigation, see Get started with the Navigation component.