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:

Groovy

dependencies {
    def nav_version = "2.5.3"

    implementation "androidx.navigation:navigation-compose:$nav_version"
}

Kotlin

dependencies {
    def nav_version = "2.5.3"

    implementation("androidx.navigation:navigation-compose:$nav_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 navbar 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 = 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:

navController.navigate("friendslist")

By default, navigate adds your new destination to the back stack. You can modify the behavior of navigate by attaching additional navigation options to our navigate() call:

// Pop everything up to the "home" destination off the back stack before
// navigating to the "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("home")
}

// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friendslist" destination
navController.navigate("friendslist") {
    popUpTo("home") { inclusive = true }
}

// Navigate to the "search” destination only if we’re not already on
// the "search" destination, avoiding multiple copies on the top of the
// back stack
navController.navigate("search") {
    launchSingleTop = true
}

See the popUpTo guide for more use cases.

The NavController's navigate function modifies the NavController's internal state. To comply with the single source of truth principle as much as possible, only the composable function or state holder that hoists the NavController instance and those composable functions that take the NavController as a parameter should make navigation calls. Navigation events triggered from other composable functions lower in the UI hierarchy need to expose those events to the caller appropriately using functions.

The following example shows MyAppNavHost composable function as the single source of truth for the NavController instance. ProfileScreen exposes an event as a function that is called when the user taps on a button. MyAppNavHost, which owns navigating to the different screens in the app, makes the navigation call to the right destination when calling ProfileScreen.

@Composable
fun MyAppNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController = rememberNavController(),
    startDestination: String = "profile"
) {
    NavHost(
        modifier = modifier,
        navController = navController,
        startDestination = startDestination
    ) {
        composable("profile") {
            ProfileScreen(
                onNavigateToFriends = { navController.navigate("friendsList") },
                /*...*/
            )
        }
        composable("friendslist") { FriendsListScreen(/*...*/) }
    }
}

@Composable
fun ProfileScreen(
    onNavigateToFriends: () -> Unit,
    /*...*/
) {
    /*...*/
    Button(onClick = onNavigateToFriends) {
        Text(text = "See friends list")
    }
}

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.

Exposing events from composable functions to callers that know how to handle a particular logic in the app is a good practice in Compose when hoisting state.

Even though exposing events as individual lambda parameters could overload the function signature, it maximizes the visibility of what the composable function responsibilities are. You can see what it does at a glance.

Other alternatives that could reduce the number of parameters in the function declaration might initially be more comfortable to write but hide some disadvantages in the long run. For example, creating a wrapper class like ProfileScreenEvents that centralizes all the events in one place. Doing this reduces the visibility of what the composable does when going through its function definition, it adds another class and methods to your project count, and you need to create and remember instances of that class every time you call that composable function anyway. Also, to reuse that wrapper class as much as possible, this pattern encourages passing an instance of that class down the UI hierarchy instead of the best practice of passing to composables just what they need.

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. The arguments parameter of composable() accepts a list of NamedNavArguments. You can quickly create a NamedNavArgument using the navArgument method and then specify its exact type:

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

You should extract the arguments 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 append it to the route when you make the navigate call:

navController.navigate("profile/user1234")

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

Retrieving complex data when navigating

It is strongly advised not to pass around complex data objects when navigating, but instead pass the minimum necessary information, such as a unique identifier or other form of ID, as arguments when performing navigation actions:

// Pass only the user ID when navigating to a new destination as argument
navController.navigate("profile/user1234")

Complex objects should be stored as data in a single source of truth, such as the data layer. Once you land on your destination after navigating, you can then load the required information from the single source of truth by using the passed ID. To retrieve the arguments in your ViewModel that's responsible for accessing the data layer, you can use the ViewModel’s SavedStateHandle:

class UserViewModel(
    savedStateHandle: SavedStateHandle,
    private val userInfoRepository: UserInfoRepository
) : ViewModel() {

    private val userId: String = checkNotNull(savedStateHandle["userId"])

    // Fetch the relevant user information from the data layer,
    // ie. userInfoRepository, based on the passed userId argument
    private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(userId)

// …

}

This approach helps prevent data loss during configuration changes and any inconsistencies when the object in question is being updated or mutated.

For a more in depth explanation on why you should avoid passing complex data as arguments, as well as a list of supported argument 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 = "user1234" })
) { backStackEntry ->
    Profile(navController, backStackEntry.arguments?.getString("userId"))
}

Now, even if there is no argument passed to the destination, the defaultValue, "user1234", is used instead.

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

Navigation Compose supports implicit deep links that can be defined as part of the composable() function as well. Its deepLinks parameter accepts a list of NavDeepLinks which can be quickly created using the navDeepLink method:

val uri = "https://www.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 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 automatically deep links 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 = LocalContext.current
val deepLinkIntent = Intent(
    Intent.ACTION_VIEW,
    "https://www.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.

Nested Navigation

Destinations can be grouped into a nested graph to modularize a particular flow in your app’s UI. An example of this could be a self-contained login flow.

The nested graph encapsulates its destinations. As with the root graph, a nested graph must have a destination identified as the start destination by its route. This is the destination that is navigated to when you navigate to the route associated with the nested graph.

To add a nested graph to your NavHost, you can use the navigation extension function:

NavHost(navController, startDestination = "home") {
    ...
    // Navigating to the graph via its route ('login') automatically
    // navigates to the graph's start destination - 'username'
    // therefore encapsulating the graph's internal routing logic
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
    ...
}

It is strongly recommended that you split your navigation graph into multiple methods as the graph grows in size. This also allows multiple modules to contribute their own navigation graphs.

fun NavGraphBuilder.loginGraph(navController: NavController) {
    navigation(startDestination = "username", route = "login") {
        composable("username") { ... }
        composable("password") { ... }
        composable("registration") { ... }
    }
}

By making the method an extension method on NavGraphBuilder, you can use it alongside the prebuilt navigation, composable, and dialog extension methods:

NavHost(navController, startDestination = "home") {
    ...
    loginGraph(navController)
    ...
}

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 bottom navigation component. Doing this allows you to navigate by selecting the icons in the bottom bar.

To use the BottomNavigation and BottomNavigationItem components, add the androidx.compose.material dependency to your Android application.

Groovy

dependencies {
    implementation "androidx.compose.material:material:1.3.1"
}

android {
    buildFeatures {
        compose true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.2"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

Kotlin

dependencies {
    implementation("androidx.compose.material:material:1.3.1")
}

android {
    buildFeatures {
        compose = true
    }

    composeOptions {
        kotlinCompilerExtensionVersion = "1.3.2"
    }

    kotlinOptions {
        jvmTarget = "1.8"
    }
}

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 current NavBackStackEntry using the currentBackStackEntryAsState() function. This entry gives you access to the current NavDestination. The selected state of each BottomNavigationItem can then be determined by comparing the item's route with the route of the current destination and its parent destinations (to handle cases when you are using nested navigation) via the [NavDestination][13]hierarchy`.

The item's route is also used to connect the onClick lambda to a call to navigate so that tapping on the item navigates to that item. By using the saveState and restoreState flags, the state and back stack of that item is correctly saved and restored as you swap between bottom navigation items.

val navController = rememberNavController()
Scaffold(
  bottomBar = {
    BottomNavigation {
      val navBackStackEntry by navController.currentBackStackEntryAsState()
      val currentDestination = navBackStackEntry?.destination
      items.forEach { screen ->
        BottomNavigationItem(
          icon = { Icon(Icons.Filled.Favorite, contentDescription = null) },
          label = { Text(stringResource(screen.resourceId)) },
          selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
          onClick = {
            navController.navigate(screen.route) {
              // Pop up to the start destination of the graph to
              // avoid building up a large stack of destinations
              // on the back stack as users select items
              popUpTo(navController.graph.findStartDestination().id) {
                saveState = true
              }
              // Avoid multiple copies of the same destination when
              // reselecting the same item
              launchSingleTop = true
              // Restore state when reselecting a previously selected item
              restoreState = true
            }
          }
        )
      }
    }
  }
) { innerPadding ->
  NavHost(navController, startDestination = Screen.Profile.route, Modifier.padding(innerPadding)) {
    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.

Type safety in Navigation Compose

The code on this page isn't type-safe. You can call the navigate() function with inexisting routes or incorrect arguments. However, you can structure your Navigation code to be type-safe at runtime. By doing so, you can avoid crashes and make sure that:

  • The arguments you provide when navigating to a destination or navigation graph are the right types and that all required arguments are present.
  • The arguments you retrieve from SavedStateHandle are the correct types.

For more information about this, check out the Navigation type safety documentation.

Interoperability

If you want to use the Navigation component with Compose, you have two options:

  • Define a navigation graph with the Navigation component for fragments.
  • Define a navigation graph with a NavHost in Compose using Compose destinations. This is possible only if all of the screens in the navigation graph are composables.

Therefore, the recommendation for mixed Compose and Views apps is to use the Fragment-based Navigation component. Fragments will then hold View-based screens, Compose screens, and screens that use both Views and Compose. Once each Fragment's contents are in Compose, the next step is to tie all of those screens together with Navigation Compose and remove all of the Fragments.

In order to change destinations inside Compose code, you expose events that can be passed to and triggered by any composable in the hierarchy:

@Composable
fun MyScreen(onNavigate: (Int) -> ()) {
    Button(onClick = { onNavigate(R.id.nav_profile) } { /* ... */ }
}

In your fragment, you make the bridge between Compose and the fragment-based Navigation component by finding the NavController and navigating to the destination:

override fun onCreateView( /* ... */ ) {
    setContent {
        MyScreen(onNavigate = { dest -> findNavController().navigate(dest) })
    }
}

Alternatively, you can pass the NavController down your Compose hierarchy. However, exposing simple functions is much more reusable and testable.

Testing

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

This means that you shouldn't pass the navController directly into any composable and instead pass navigation callbacks as parameters. This allows all your composables to be individually testable, as they don't require an instance of navController in tests.

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

This way, 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 = "user1234" })
) { backStackEntry ->
    Profile(backStackEntry.arguments?.getString("userId")) { friendUserId ->
        navController.navigate("profile?userId=$friendUserId")
    }
}

It is recommended to write tests that cover your app navigation requirements by testing the NavHost, navigation actions passed to your composables as well as your individual screen composables.

Testing the NavHost

To begin testing your NavHost , add the following navigation-testing dependency:

dependencies {
// ...
  androidTestImplementation "androidx.navigation:navigation-testing:$navigationVersion"
  // ...
}

You can set up your NavHost test subject and pass an instance of the navController instance to it. For this, the Navigation testing artifact provides a TestNavHostController. A UI test that verifies the start destination of your app and NavHost would look like this:

class NavigationTest {

    @get:Rule
    val composeTestRule = createComposeRule()
    lateinit var navController: TestNavHostController

    @Before
    fun setupAppNavHost() {
        composeTestRule.setContent {
            navController = TestNavHostController(LocalContext.current)
            navController.navigatorProvider.addNavigator(ComposeNavigator())
            AppNavHost(navController = navController)
        }
    }

    // Unit test
    @Test
    fun appNavHost_verifyStartDestination() {
        composeTestRule
            .onNodeWithContentDescription("Start Screen")
            .assertIsDisplayed()
    }
}

Testing navigation actions

You can test your navigation implementation in multiple ways, by performing clicks on the UI elements and then either verifying the displayed destination or by comparing the expected route against the current route.

As you want to test your concrete app's implementation, clicks on the UI are preferable. To learn how to test this alongside individual composable functions in isolation, make sure to check out the Testing in Jetpack Compose codelab.

You also can use the navController to check your assertions by comparing the current String route to the expected one, using navController's currentBackStackEntry:

@Test
fun appNavHost_clickAllProfiles_navigateToProfiles() {
    composeTestRule.onNodeWithContentDescription("All Profiles")
        .performScrollTo()
        .performClick()

    val route = navController.currentBackStackEntry?.destination?.route
    assertEquals(route, "profiles")
}

For more guidance on Compose testing basics, go through the Compose Testing documentation and Testing in Jetpack Compose codelab. To learn more about advanced testing of navigation code, visit the Test Navigation guide.

Learn more

To learn more about Jetpack Navigation, see Get started with the Navigation component or take the Jetpack Compose Navigation codelab.

To learn how to design your app's navigation so it adapts to different screen sizes, orientations, and form factors, see Navigation for responsive UIs.

To learn about more advanced Navigation Compose implementation in a modularized app, including concepts like nested graphs and bottom navigation bar integration, take a look at the Now in Android repository.

Samples