In most Android apps, It’s common to have several screens with bottom navigation. In this article, I explain how to manage the navigation stuff in a Jetpack compose app, mention some common issues that may face, and the solution that may take.
Let’s assumed we are working on a rich multi-modular production code-base and starting to migrate it to Jetpack compose. how we deal with the navigation stuff? Here, we have two fields; the bottom navigation and navigation between screens.
Setup Navigation basics
First of all, with Scaffold
setup aNavHost
in MainActivity and this will be the main navigation graph:
@Composable fun MyAppNavHost( navController: NavHostController, modifier: Modifier, ) { NavHost( navController = navController, startDestination = Destinations.NewsListScreen.route, modifier = modifier, ) { // bottom navigation screens & nested graphs newsListGraph(navController) favoriteNewsGraph(navController) profileGraph(navController) // common screens in entire app newsDetailGraph() ... } }
For prevent to create a huge main navGraph, we split it to separate screens and nested graphs. In fact, every screen has its own composable wrapped by a NavGraphBuilder
extension function. For a common screen be like:
fun NavGraphBuilder.VerifyCodeGraph() { composable( route = Destinations.VerifyCodeScreen().route, ) { VerifyCodeScreen() } }
For managing the route of every screen, we can use a sealed class like this:
sealed class Destinations(val route: String) { object NewsListScreen : Destinations("news_list_screen") data class NewsDetailScreen(val news: String = "news") : Destinations("news_detail_screen") object FavoriteNewsScreen : Destinations("favorite_news_screen") object ProfileScreen : Destination("profile_screen") object SettingScreen : Destination("setting_screen") object ThemeScreen : Destination("theme_screen") object LoginScreen : Destination("login_screen") object VerifyCodeScreen : Destination("verify_code_screen") ... }
Also, if any screen needs some parameters, a data class
could be embedded in the screen like data class NewsDetailScreen(val news: String = “news”)
.
If any screen needs to navigate to another screen, simply just pass a function to handle that:
fun NavGraphBuilder.settingListGraph( navController: NavController, ) { composable(Destinations.SettingScreen.route) { NewsListRoute( onNavigateToThemeScreen = { navController.navigate( route = Destinations.ThemeScreen().route, ) } ) } }
In this way, there is no need to depend your feature module on the navigation library. There will be the app module responsibility to handle navigation stuff.
If some screens can be grouped in a nested graph, do like this:
fun NavGraphBuilder.profileGraph( navController: NavHostController ) { navigation( startDestination = Destination.ProfileScreen.route, route = Destination.ProfileScreen.route.addGraphPostfix(), ) { composable(Destination.ProfileScreen.route) { ProfileScreen( onNavigationToLoginScreen = { navController.navigate( route = Destination.LoginScreen.route.addGraphPostfix(), ) } ) } loginGraph() } }
For nested graphs, Consider these tips:
- Every nested navGraph must have a
startDestination
- Every nested navGraph must have a unique route like other
composable
. A simple solution is using thestartDestination
route +“_graph”
- You can easily add other
composable
and nested graphs to this graph
To setup the bottom navigation bar, simply follow the doc. If you want to have Deeplink, follow this section.
Job Offers
Passing arguments
Another need in navigation is passing some arguments. For passing primitive data, you can follow the doc. But in some scenarios, it needs to pass an object. Currently(at the begging of 2023), there is no solution when using navigate()
with a route, but there is an overload that accept a bundle
:
public open fun navigate( @IdRes resId: Int, args: Bundle?, navOptions: NavOptions?, navigatorExtras: Navigator.Extras? )
As you can see, it gets an id for the destination. Let’s write an extension function so we can use it:
fun NavController.navigate( route: String, args: Bundle, navOptions: NavOptions? = null, navigatorExtras: Navigator.Extras? = null ) { val routeLink = NavDeepLinkRequest .Builder .fromUri(NavDestination.createRoute(route).toUri()) .build() val deepLinkMatch = graph.matchDeepLink(routeLink) if (deepLinkMatch != null) { val destination = deepLinkMatch.destination val id = destination.id navigate(id, args, navOptions, navigatorExtras) } else { navigate(route, navOptions, navigatorExtras) } }
And for using this function in the source screen:
fun NavGraphBuilder.newsListGraph( navController: NavController, ) { composable(Destinations.NewsListScreen.route) { NewsListScreen( onNavigateToDetailScreen = { news -> navController.navigate( route = Destinations.NewsDetailScreen().route, args = bundleOf(Destinations.NewsDetailScreen().news to news) ) } ) } }
In the destination screen:
fun NavGraphBuilder.newsDetailScreen() { composable( route = Destinations.NewsDetailScreen().route, ) { entry -> val news = entry.parcelableData<News>(Destinations.NewsDetailScreen().news) NewsDetailScreen(news = news,) } } inline fun <T> NavBackStackEntry.parcelableData(key: String): T? { return arguments?.parcelable(key) as? T }
In the end, you can see the below GitHub repository for the implementation of the above tips in a code-base.
Have fun! ✌🏻
This article was originally published on proandroiddev.com