0% found this document useful (0 votes)
83 views15 pages

Navigation With Compose - Jetpack Compose - Android Developers

Uploaded by

mywire.ac.01
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
83 views15 pages

Navigation With Compose - Jetpack Compose - Android Developers

Uploaded by

mywire.ac.01
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 15

Navigation with Compose

The Navigation component (/guide/navigation) 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 (/jetpack/compose)
resources before continuing.

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

GroovyKotlin (#kotlin)
(#groovy)

dependencies {
def 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 (/guide/navigation/get-started) overview.

Create a NavController
For information on how to create a NavController in Compose, see the Compose
section of Create a navigation controller (/guide/navigation/navcontroller).
Create a NavHost
For information on how to create a NavHost in Compose, see the Compose section of
Design your navigation graph (/guide/navigation/design#compose).

Navigate to a composable
For information on navigating to a Composable, see Navigate to a destination
(/guide/navigation/use-graph/navigate) 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
(/guide/navigation/navigation-deep-link#implicit) 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
(/reference/androidx/navigation/NamedNavArgument) objects. You can quickly create a
NamedNavArgument using the navArgument()
(/reference/kotlin/androidx/navigation/package-
summary#navArgument(kotlin.String,kotlin.Function1))
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


(/reference/kotlin/androidx/navigation/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


(/guide/navigation/navigation-pass-data#supported_argument_types).

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 (/topic/libraries/architecture/viewmodel-savedstate#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(use

// …

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 (/guide/navigation/navigation-pass-data#supported_argument_types).

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 have nullable = 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.

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
(/reference/androidx/navigation/NavDeepLink) objects which can be quickly created using the
navDeepLink()
(/reference/kotlin/androidx/navigation/package-summary#navDeepLink(kotlin.Function1)) 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).


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
(/guide/navigation/design/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


(/reference/kotlin/androidx/compose/material/package-
summary#BottomNavigation(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androi
dx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,kotlin.Function1))
and BottomNavigationItem
(/reference/kotlin/androidx/compose/material/package-summary#
(androidx.compose.foundation.layout.RowScope).BottomNavigationItem(kotlin.Boolean,kotlin.Function
0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function0,kotlin.Boolean,androi
dx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Color,and
roidx.compose.ui.graphics.Color))
components, add the androidx.compose.material dependency to your Android
application.

GroovyKotlin (#kotlin)
(#groovy)

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

android {
buildFeatures {
compose true
}

composeOptions {
kotlinCompilerExtensionVersion = "1.5.14"
}

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
(/reference/kotlin/androidx/compose/material/package-summary#
(androidx.compose.foundation.layout.RowScope).BottomNavigationItem(kotlin.Boolean,kotlin.Function
0,kotlin.Function0,androidx.compose.ui.Modifier,kotlin.Boolean,kotlin.Function0,kotlin.Boolean,androi
dx.compose.foundation.interaction.MutableInteractionSource,androidx.compose.ui.graphics.Color,and
roidx.compose.ui.graphics.Color))
:

val items = listOf(


Screen.Profile,
Screen.FriendsList,
)

In your BottomNavigation
(/reference/kotlin/androidx/compose/material/package-
summary#BottomNavigation(androidx.compose.ui.Modifier,androidx.compose.ui.graphics.Color,androi
dx.compose.ui.graphics.Color,androidx.compose.ui.unit.Dp,kotlin.Function1))
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
(#nested-nav)) using the NavDestination (/reference/androidx/navigation/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.
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.pad
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 Type safe, multi-module be…
be…
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, see Type safety in Kotlin DSL and Navigation Compose
(/guide/navigation/navigation-type-safety).

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
(/guide/navigation/design) 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:$navigatio
// ...
}

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
(/reference/kotlin/androidx/navigation/testing/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 (/codelabs/jetpack-compose-testing) 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
(/develop/ui/compose/testing) and the Testing in Jetpack Compose
(/codelabs/jetpack-compose-testing) codelab. To learn more about advanced testing of
navigation code, visit the Test Navigation (/guide/navigation/navigation-testing) guide.

Learn more
To learn more about Jetpack Navigation, see Get started with the Navigation component
(/guide/navigation/navigation-getting-started) or take the Jetpack Compose Navigation
codelab (/codelabs/jetpack-compose-navigation).

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
(/guide/topics/large-screens/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 (https://github.com/android/nowinandroid) app on GitHub.

Samples
(https://github.com/android/co (https://github.com/android/no (https://github.com/android/su
mpose- winandroid/tree/main) nflower/tree/main)
samples/tree/main/JetNews)
GITHUB GITHUB GITHUB

Jetnews sample Now in Android Sunflower with


(https://github.com/android/co App Compose
mpose-
(https://github.com/android/no (https://github.com/android/su
samples/tree/main/JetNews)
winandroid/tree/main) nflower/tree/main)

Jetnews is a sample news


Learn how this app was A gardening app illustrating
reading app, built with
designed and built in the Android development best
Jetpack Compose. The
design case study, practices with migrating a
goal of the sample is to

Content and code samples on this page are subject to the licenses described in the Content License
(/license). Java and OpenJDK are trademarks or registered trademarks of Oracle and/or its affiliates.

Last updated 2024-06-13 UTC.

You might also like