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 { implementation "androidx.navigation:navigation-compose:1.0.0-alpha08" }
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(...) }
...
}
Navigate to a composable
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.
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 "friends" destination
navController.navigate(“friends”) {
popUpTo("home")
}
// Pop everything up to and including the "home" destination off
// the back stack before navigating to the "friends" destination
navController.navigate("friends") {
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.
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. 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 havenullability = 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 = "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.
Deep links
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.
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 = startRoute) {
...
navigation(startDestination = nestedStartRoute, route = nested) {
composable(nestedStartRoute) { ... }
}
...
}
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 = {
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.startDestination
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
}
}
)
}
}
}
) {
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.