Navigation in Compose

Navigation in Compose

Navigation 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.
Note: If you are not familiar with Compose, review the Jetpack Compose resources before continuing.

Setup

To support Compose, use the following dependency in your app module's build.gradle file:
dependencies { val nav_version = "2.8.1" implementation("androidx.navigation:navigation-compose:$nav_version") }

Get started

When implementing navigation in an app, implement a navigation host, graph, and controller. For more information, see the Navigation overview.

Create a NavController

For information on how to create a NavController in Compose, see the Compose section of Create a navigation controller.
 
To create a NavController when using Jetpack Compose, call rememberNavController():
val navController = rememberNavController()
You should create the NavController high in your composable hierarchy. It needs to be high enough that all the composables that need to reference it can do so.
Doing so lets you to use the NavController as the single source of truth for updating composables outside of your screens. This follows the principles of state hoisting

Create a NavHost

In Compose, use a serializable object or class to define a route. A route describes how to get to a destination, and contains all the information that the destination requires. Once you have defined your routes, use the NavHost composable to create your navigation graph. Consider the following example:
@Serializable object Profile @Serializable object FriendsList val navController = rememberNavController() NavHost(navController = navController, startDestination = Profile) {     composable<Profile> { ProfileScreen( /* ... */ ) }     composable<FriendsList> { FriendsListScreen( /* ... */ ) }     // Add more destinations similarly. }
  1. A serializable object represents each of the two routes, Profile and FriendsList.
  1. The call to the NavHost composable passes a NavController and a route for the start destination.
  1. The lambda passed to the NavHost ultimately calls NavController.createGraph() and returns a NavGraph.
  1. Each route is supplied as a type argument to NavGraphBuilder.composable<T>() which adds the destination to the resulting NavGraph.
  1. The lambda passed to composable is what the NavHost displays for that destination.
Caution: Instead of passing a type to composable(), you can pass a route string or an integer id. However, this makes it much more difficult to manage passing additional arguments to the destination.

Understand the lambda

To better understand the lambda that creates the NavGraph, consider that to build the same graph as in the preceding snippet, you could create the NavGraph separately using NavController.createGraph() and pass it to the NavHost directly:
val navGraph by remember(navController) {   navController.createGraph(startDestination = Profile)) {     composable<Profile> { ProfileScreen( /* ... */ ) }     composable<FriendsList> { FriendsListScreen( /* ... */ ) }   } } NavHost(navController, navGraph)
⚠️
Important: A NavController is associated with a single NavHost composable. The NavHost provides the NavController access to its navigation graph. When you use the NavController to navigate to a destination, you cause the NavController to interact with its associated NavHost.

Pass arguments

If you need to pass data to a destination, define the route with a class that has parameters. For example, the Profile route is a data class with a name parameter.
@Serializable data class Profile(val name: String)
Whenever you need to pass arguments to that destination, you create an instance of your route class, passing the arguments to the class constructor.
Note: Use a data class for a route with arguments, and an object or data object for a route with no arguments.
For optional arguments, create nullable fields with a default value.
@Serializable data class Profile(val nickname: String? = null)

Obtain route instance

You can obtain the route instance with NavBackStackEntry.toRoute() or SavedStateHandle.toRoute(). When you create a destination using composable(), the NavBackStackEntry is available as a parameter.
@Serializable data class Profile(val name: String) val navController = rememberNavController() NavHost(navController = navController, startDestination = Profile(name="John Smith")) {     composable<Profile> { backStackEntry ->         val profile: Profile = backStackEntry.toRoute()         ProfileScreen(name = profile.name) } }
Note the following in this snippet:
  • The Profile route specifies the starting destination in the navigation graph, with "John Smith" as the argument for name.
  • The destination itself is the composable<Profile>{} block.
  • The ProfileScreen composable takes the value of profile.name for its own name argument.
  • As such, the value "John Smith" passes through to ProfileScreen.

Minimal example

A complete example of a NavController and NavHost working together:
@Serializable data class Profile(val name: String) @Serializable object FriendsList // Define the ProfileScreen composable. @Composable fun ProfileScreen(     profile: Profile     onNavigateToFriendsList: () -> Unit,   ) {   Text("Profile for ${profile.name}")   Button(onClick = { onNavigateToFriendsList() }) {     Text("Go to Friends List")   } } // Define the FriendsListScreen composable. @Composable fun FriendsListScreen(onNavigateToProfile: () -> Unit) {   Text("Friends List")   Button(onClick = { onNavigateToProfile() }) {     Text("Go to Profile")   } } // Define the MyApp composable, including the `NavController` and `NavHost`. @Composable fun MyApp() {   val navController = rememberNavController()   NavHost(navController, startDestination = Profile(name = "John Smith")) {     composable<Profile> { backStackEntry ->         val profile: Profile = backStackEntry.toRoute()         ProfileScreen(             profile = profile,             onNavigateToFriendsList = {                 navController.navigate(route = FriendsList)             }         )     }     composable<FriendsList> {       FriendsListScreen(         onNavigateToProfile = {           navController.navigate(             route = Profile(name = "Aisha Devi")           )         }       )     }   } }
As the snippet demonstrates, instead of passing the NavController to your composables, expose an event to the NavHost. That is, your composables should have a parameter of type () -> Unit for which the NavHost passes a lambda that calls NavController.navigate().
Note: By using the parameters of the route class you can pass data to the given destination with full type safety. For example, in the previous code Profile.name ensures that name is always a String.
 

Navigate to a composable

Use a NavController

The key type you use to move between destinations is the NavController. See Create a navigation controller for more information on the class itself and how to create an instance of it. This guide details how to use it.

Navigate

Regardless of which UI framework you use, there is a single function you can use to navigate to a destination: NavController.navigate().
There are many overloads available for navigate(). The overload you should choose corresponds to your exact context. For example, you should use one overload when navigating to a composable and another when navigating to a view.
The following sections outline some of the key navigate() overloads you can use.

Navigate to a composable

To navigate to a composable, you should use NavController.navigate<T>. With this overload, navigate() takes a single route argument for which you pass a type. It serves as the key to a destination.
@Serializable object FriendsList navController.navigate(route = FriendsList)
To navigate to a composable in the navigation graph, first define your NavGraph such that each destination corresponds to a type. For composables, you do so with the composable() function.

Expose events from your composables

When a composable function needs to navigate to a new screen, you shouldn't pass it a reference to the NavController so that it can call navigate() directly. According to Unidirectional Data Flow (UDF) principles, the composable should instead expose an event that the NavController handles.
More directly put, your composable should have a parameter of type () -> Unit. When you add destinations to your NavHost with the composable() function, pass your composable a call to NavController.navigate().
See the following subsection for an example of this.
Warning: Don't pass your NavController to your composables. Expose an event as described here.

Example

As a demonstration of the preceding sections, observe these points in the following snippet:
  1. Each destination in the graph is created using a route, which is a serializable object or class describing the data required by that destination.
  1. The MyAppNavHost composable holds the NavController instance.
  1. Accordingly, calls to navigate() should occur there and not in a lower composable like ProfileScreen.
  1. ProfileScreen contains a button that navigates the user to FriendsList when clicked. However, it does not call navigate() itself.
  1. Instead, the button calls a function that is exposed as the parameter onNavigateToFriends.
  1. When MyAppNavHost adds ProfileScreen to the navigation graph, for onNavigateToFriends it passes a lambda that calls navigate(route = FriendsList).
  1. This ensures that when the user presses the button ProfileScreen, they navigate correctly to FriendsListScreen.
@Serializable object Profile @Serializable object FriendsList @Composable fun MyAppNavHost(     modifier: Modifier = Modifier,     navController: NavHostController = rememberNavController(), ) {     NavHost(         modifier = modifier,         navController = navController,         startDestination = Profile     ) {         composable<Profile> {             ProfileScreen(                 onNavigateToFriends = { navController.navigate(route = FriendsList) },                 /*...*/             )         }         composable<FriendsList> { FriendsListScreen(/*...*/) }     } } @Composable fun ProfileScreen(     onNavigateToFriends: () -> Unit,     /*...*/ ) {     /*...*/     Button(onClick = onNavigateToFriends) {         Text(text = "See friends list")     } }
Warning: You should only call navigate() as part of a callback and not as part of your composable itself. This avoids calling navigate() on every recomposition.

Navigate with arguments

For information on passing arguments between composable destinations, see the Compose section of Design your navigation graph.

Retrieve 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(id = "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, use the SavedStateHandle of the ViewModel:
class UserViewModel(     savedStateHandle: SavedStateHandle,     private val userInfoRepository: UserInfoRepository ) : ViewModel() {     private val profile = savedStateHandle.toRoute<Profile>()     // Fetch the relevant user information from the data layer,     // ie. userInfoRepository, based on the passed userId argument     private val userInfo: Flow<UserInfo> = userInfoRepository.getUserInfo(profile.id) // … }
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.

Deep links

Navigation Compose supports deep links that can be defined as part of the composable() function as well. Its deepLinks parameter accepts a list of NavDeepLink objects which can be quickly created using the navDeepLink() method:
@Serializable data class Profile(val id: String) val uri = "https://www.example.com" composable<Profile>(   deepLinks = listOf(     navDeepLink<Profile>(basePath = "$uri/profile")   ) ) { backStackEntry ->   ProfileScreen(id = backStackEntry.toRoute<Profile>().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 in the preceding example, 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/profile/$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

For information on how to create nested navigation graphs, see Nested graphs.

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 lets you 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.
dependencies { implementation("androidx.compose.material:material:1.7.2") } android { buildFeatures { compose = true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } 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 class, such as TopLevelRoute seen here, that has a route class and an icon.
data class TopLevelRoute<T : Any>(val name: String, val route: T, val icon: ImageVector)
Then place those routes in a list that can be used by the BottomNavigationItem:
val topLevelRoutes = listOf(    TopLevelRoute("Profile", Profile, Icons.Profile),    TopLevelRoute("Friends", Friends, Icons.Friends) )
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 using the NavDestination 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       topLevelRoutes.forEach { topLevelRoute ->         BottomNavigationItem(           icon = { Icon(topLevelRoute.icon, contentDescription = topLevelRoute.name) },           label = { Text(topLevelRoute.name) },           selected = currentDestination?.hierarchy?.any { it.hasRoute(topLevelRoute.route::class) } == true,           onClick = {             navController.navigate(topLevelRoute.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 = Profile, Modifier.padding(innerPadding)) {     composable<Profile> { ProfileScreen(...) }     composable<Friends> { FriendsScreen(...) }   } }
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.

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.

Navigate from Compose with Navigation for 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) -> Unit) {     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

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 lets you 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 ProfileScreen 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 ProfileScreen(     userId: String,     navigateToFriendProfile: (friendUserId: String) -> Unit ) {  … }
This way, the ProfileScreen 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:
@Serializable data class Profile(id: String) composable<Profile> { backStackEntry ->     val profile = backStackEntry.toRoute<Profile>()     ProfileScreen(userId = profile.id) { friendUserId ->         navController.navigate(route = Profile(id = 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"   // ... }
Wrap your app's NavHost in a composable which accepts a NavHostController as a parameter.
@Composable fun AppNavHost(navController: NavHostController){   NavHost(navController = navController){ ... } }
Now you can test AppNavHost and all the navigation logic defined inside NavHost by passing an instance of the navigation testing artifact 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 route to the expected one, using navController's currentBackStackEntry:
@Test fun appNavHost_clickAllProfiles_navigateToProfiles() {     composeTestRule.onNodeWithContentDescription("All Profiles")         .performScrollTo()         .performClick()     assertTrue(navController.currentBackStackEntry?.destination?.hasRoute<Profile>() ?: false) }
For more guidance on Compose testing basics, see Testing your Compose layout and the Testing in Jetpack Compose codelab. To learn more about advanced testing of navigation code, visit the Test Navigation guide.