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.7.7" implementation "androidx.navigation:navigation-compose:$nav_version" }
Kotlin
dependencies { val nav_version = "2.7.7" 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.
Create a NavHost
For information on how to create a NavHost
in Compose, see the Compose section
of Design your navigation graph.
Navigate to a composable
For information on navigating to a Composable, see Navigate to a destination in the architecture documentation.
Navigate with arguments
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 NamedNavArgument
objects. 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.
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/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 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.
Add 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 havenullable = true
(which implicitly sets the default value tonull
)
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.
Deep links
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
NavDeepLink
objects 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 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/$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.
Groovy
dependencies { implementation "androidx.compose.material:material:1.6.8" } android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion = "1.5.15" } kotlinOptions { jvmTarget = "1.8" } }
Kotlin
dependencies { implementation("androidx.compose.material:material:1.6.8") } 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 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 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
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 nonexistent 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, see Type safety in Kotlin DSL and Navigation Compose.
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 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, 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.
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 a more advanced Compose navigation implementation in a modularized app, including concepts like nested graphs and bottom navigation bar integration, take a look at the Now in Android app on GitHub.
Samples
Recommended for you
- Note: link text is displayed when JavaScript is off
- Material Design 2 in Compose
- Migrate Jetpack Navigation to Navigation Compose
- Where to hoist state