Jetpack Compose is a declarative framework for building native Android UI recommended by Google. To simplify and accelerate UI development, the framework turns the traditional model of Android UI development on its head. Rather than constructing UI by imperatively controlling views defined in XML, UI is built in Jetpack Compose by composing functions that define how app data is transformed into UI.
An app built entirely in Compose may consist of a single Activity that hosts a composition, meaning that the fragment-based Navigation Architectural Components can no longer be used directly in such an application. Fortunately, navigation-compose provides a compatibility layer for interacting with the Navigation Component from Compose.
The androidx.navigation:navigation-compose dependency provides an API for Compose apps to interact with the Navigation Component, taking advantage of its familiar features, including handling up and back navigation and deep links.
The Navigation Component consists of three parts:NavController,NavHost, and the navigation graph.
TheĀ NavController is the class through which the Navigation Component is accessed. It is used to navigate between destinations and maintains each destinationās state and the back stackās state. An instance of the NavControllerĀ is obtained through theĀ rememberNavController()Ā method as shown:
val navController = rememberNavController()
TheĀ NavHost, as the name indicates, serves as a host or container for the current navigation destination. TheĀ NavHostĀ also links theĀ NavControllerĀ with the navigation graph (described below). Creating aĀ NavHostĀ requires an instance ofĀ NavController, obtained throughĀ rememberNavController()Ā as described above, and aĀ String representing the route associated with the starting point of navigation.
NavHost(navController = navController, startDestination = "home") {
...
}
In the fragment-based manifestation of the Navigation Component, the navigation graph consists of an XML resource that describes all destinations and possible navigation paths throughout the app. In Compose, the navigation graph is built using the lambda syntax from theĀ Navigation Kotlin DSLĀ instead of XML. The navigation graph is constructed in the trailing lambda passed toĀ NavHost as shown below:
NavHost(navController = navController, startDestination = "home") {
composable("home") { MealsListScreen() }
composable("details") { MealDetailsScreen() }
}
In this example, theĀ MealsListScreen()Ā composable is associated with the route defined by theĀ String āhome,ā and theĀ MealDetailsScreen()Ā composable is associated with the ādetailsā route. TheĀ startDestination is set to āhome,ā meaning that theĀ MealsListScreen()Ā composable will be displayed when the app launches.
Note that in the example above, the lambda is passed to the builderĀ parameter of theĀ NavHost function, which has a receiver type ofĀ NavGraphBuilder. This allows for the concise syntax for providing composable destinations to the navigation graph throughĀ NavGraphBuilder.composable().
TheĀ NavGraphBuilder.composable()Ā method has a requiredĀ routeĀ parameter that is aĀ StringĀ representing each unique destination on the navigation graph. The composable associated with the destination route is passed to theĀ contentĀ parameter using trailing lambda syntax.
TheĀ navigateĀ method ofĀ NavControllerĀ is used to navigate to a destination:
navController.navigate("details")
While it may be tempting to pass theĀ NavController instance down to composables that will trigger navigation, it is best practice not to do so. Centralizing your appās navigation code in one place makes it easier to understand and maintain. Furthermore, individual composables may appear or behave differently on different screen sizes. For example, a button may result in navigation to a new screen on a phone but not on tablets. Therefore it is best practice to pass functions down to composables for navigation-related events that can be handled in the composable that hosts theĀ NavController.
For example, imagineĀ MealsListScreenĀ takes anĀ onItemClick: () -> UnitĀ parameter. You could then handle that event in the composable that containsĀ NavHostĀ as follows:
NavHost(navController = navController, startDestination = "home") {
composable("home") {
MealsListScreen(onItemClick = { navController.navigate("details") })
}
...
}
Arguments can be passed to a navigation destination by including argument placeholders within the route. If you wanted to extend the example above and pass a string representing an id for the details screen, you would first add a placeholder to the route:
NavHost(navController = navController, startDestination = "home") {
Ā ...
Ā composable("details/{mealId}") { MealDetailsScreen(...) }
}
Then you would add an argument toĀ composable, specifying its name and type:
composable(
Ā "details/{mealId}",
Ā arguments = listOf(navArgument("mealId") { type = NavType.StringType })
) { backStackEntry ->
Ā MealDetailsScreen(...)
}
Then, you would need to update calls that navigate to the destination by passing the id as part of the route:
navController.navigate("details/1234")
Finally, you would retrieve the argument from theĀ NavBackStackEntryĀ that is available within theĀ contentĀ parameter ofĀ composable():
composable(
Ā "details/{mealId}",
Ā arguments = listOf(navArgument("mealId") { type = NavType.StringType })
) { backStackEntry ->
Ā MealDetailsScreen(mealId = backStackEntry.arguments?.getString("mealId"))
}
One of the key benefits of using the Navigation Component is the automatic handling of deep links. Because routes are defined as strings that mimic URIs by convention, they can be built to correspond to the same patterns used for deep links into your app. Carrying forward with the example above and assuming that it is associated with a fictitious web property at https://bignerdranch.com/cookbookĀ you would first add the following intent filter toĀ AndroidManifest.xmlĀ to enable the app to receive the appropriate deep links:
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="bignerdranch.com"
android:pathPrefix="/cookbook"
android:scheme="https" />
</intent-filter>
Then you would update your composable destination to handle deep links of the pattern https://bignerdranch.com/cookbook/{mealId}Ā by passing a value to theĀ deepLinksĀ parameter as shown:
composable(
Ā "details/{mealId}",
Ā arguments = listOf(navArgument("mealId") { type = NavType.StringType }),
Ā deepLinks = listOf(navDeepLink { uriPattern = "https://bignerdranch.com/cookbook/{mealId}" })
) { backStackEntry ->
Ā MealDetailsScreen(mealId = backStackEntry.arguments?.getString("mealId"))
}
These deep links could be tested using an ADB command such as:
adb shell am start -d https://bignerdranch.com/cookbook/1234
In the above demonstrations, string literals were used to define routes and navigation argument names for clarity and simplicity. It is best practice to store these strings as constants or in some other construct to reduce repetition and prevent typo-based bugs. A cleaner implementation of the above example might look like this:
Ā
interface Destination {
val route: String
Ā val title: Int
}
object Home : Destination {
Ā override val route: String = "home"
Ā override val title: Int = R.string.app_name
}
object Details: Destination {
Ā override val route: String = "details"
Ā override val title: Int = R.string.meal_details
Ā const val mealIdArg = "mealId"
Ā val routeWithArg: String = "$route/{$mealIdArg}"
Ā val arguments = listOf(navArgument(mealIdArg) { type = NavType.StringType })
Ā fun getNavigationRouteToMeal(mealId: String) = "$route/$mealId"
}
...
NavHost(
Ā navController = navController,
Ā startDestination = Home.route
) {
composable(Home.route) {
Ā MealsListScreen(onItemClick = {
Ā navController.navigate(Details.getNavigationRouteToMeal(it))
})
}
composable(
Details.routeWithArg,
arguments = Details.arguments
) { backStackEntry ->
MealDetailsScreen(
mealId = backStackEntry.arguments?.getString(Details.mealIdArg) ?: ""
)
}
}
Lack of argument type safety
The primary drawback is the lack of type safety for passing arguments. While this may not seem like a big deal if you are following theĀ best practiceĀ of not passing complex data in navigation arguments, it would still be preferable to have compile-time assurance, even for simple types.
Repetitive and cumbersome API for passing arguments
In addition to the lack of type safety, the API for defining argument types and parsing them from theĀ BackStackEntryĀ is fairly repetitive and cumbersome. It involves a fair amount of potentially tricky string concatenation to build routes.
Many developers have grown to enjoy using the Navigation Editor to get a visual representation of the navigation graph for their apps and to quickly and easily define navigation actions. There is no comparable tool for Compose.
Use Fragments to host Compose
Perhaps the most straightforward alternative, especially if youāre already accustomed to the fragment-based Navigation component, would be to use Fragments to host each screen-level composable. This would carry the benefit of type-safe navigation arguments and access to the Navigation Editor.
Third-party alternatives
As a result of the drawbacks above, several third-party tools, such as Compose DestinationsĀ andĀ VoyagerĀ have been developed. For a detailed overview and comparison of these alternatives, we recommend thisĀ article.
