Navigation is the key part in application development and Android is no exception. From Activities and Fragments transitions to Navigation Component and now, Navigation Component is available for Jetpack Compose! In this article I would like to give a brief overview of how Jetpack Compose Navigation works, the problems I've faced and my solution for it. Lets get started π
When using original Navigation Component we used XML to describe our navigation. There we could declare the arguments for our destination and transitions between them. In Compose world its, of course, we use functions :)
We start by declaring a NavHost
and then we can build our navigatoin graph by adding destinations using composable
. This function takes a path URI and a @Composable as the destination content
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(/*...*/) }
composable("details") { DetailsScreen(/*...*/) }
}
We've just set up our first destinations, lets add some action into our code by addding a transition from"home
" to "details"
. Compose Navigation Component makes it pretty easy as well, all we need to do is to tell our navController
the path we want to navigate to
/**/
composable("home") {
HomeScreen(onClick: { navController.navigate("details") })
}
Lets Imagine our details route needs two arguments: an optional Int
called number and a mandatory String
called id. To support an ability to pass the arguments you need to do two things: adjust the path so that Navigation Component knows that this route can accept arguments and describe the type of the arguments. This is how it looks like in our case
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(/*...*/) }
composable("details/{id}?number={number}",
arguments = listOf(
navArgument("id") { type = NavType.StringType }
navArgument("number") { type = NavType.IntType; defaultValue = 1 }
)
) {...}}
Now for our detail transtion, it won't be enough to specify just the destination name like we used to - we need to provide arguments too, at least id anyway, as the optional param will be set to our default value.
navController.navigate("details/myFancyId?number=21")
Next step is to obtain the arguments. This can be done in multiple ways, either from the NavBackStackEntry
or from the SavedStateHandle
(this can injected in our ViewModel
)
Not that in both cases, the value you receive can be null
, so that must be addresed in some way.
/**
* Get args from NavBackStackEntry
**/
composable(/** our details route **/ ) { backStackEntry ->
val id: String? = backStackEntry.arguments?.getString("id")
val number: Int? = backStackEntry.arguments?.getInt("number")
DetailsScreen(id, number)
}
/**
* Get args from SavedStateHandle
**/
class DetailsScreenViewModel @Inject constructor(
savedStateHandle: SavedStateHandle
) {
init {
val id: String? = savedStateHandle.get<String>("number")
val number: Int? = savedStateHandle.get<Int>("name")
}
}
By now you have probably noticed how much boilerplate code we have to just support these two routes and more importantly, how many hard-coded strings we have. Each navigation stage: declarartion, arguments handling and navigation actions, they all use the path and parameters. Moreover, the arguments handling is very poor since always need to be aware of the argument name and its type.
To reduce this overhead you can wrap your destinations in a sealed class
for example, that will clean up the code for sure, but it won't solve all the problems.
Remember original Navigation Component? Well it has safeArgs
, a very useful plugin that generates code that allows to handle arguments in very clean and safe way. When I started to work with Jetpack Compose and its Navigation Component I immediately noticed that I miss this functionality, so I've decided to write on myself π¨βπ»
At it's core, safe-routing
is an annotation processing library that generates a code that will handle most of the boilerplate code and hardcoded strings an will provide a safe way to navigate between you composables and ease up arguments handling
in your project level build.gradle
allprojects {
repositories {
...
maven { url 'https://jitpack.io' }
}
}
And then in you app level build.gradle
dependencies {
kapt("com.github.levinzonr.compose-safe-routing:compiler:1.0.0")
implementation("com.github.levinzonr.compose-safe-routing:core:1.0.0")
}
Lets sync our project and use apply some annotations to clean up our code. safe-routing
provides two annotations for you describe the routes. Meet @Route
and @RouteArg
@Route
is used to describe your Route, its constructor accepts a route name and the arguments for it@RouteArg
is used to describe the arguments for your Route, it accepts several parameters: the name, type, a flag to tell whether that argument is optional or not and a String representation of the default value. Note that the default value will only be used in case the argument is optional.
Okay, now its time to revisit our navigation code π
To start, all we have to do is to apply the annotations four out two routes
@Composable
@Route("details", args = [
RouteArg("id", RouteArgType.StringType, false),
RouteArg("number", RouteArgType.IntType, true, defaultValue = 1.toString()),
])
fun DetailsScreen() {
/** your sweet composable code **/
}
// aand
@Composable
@Route("profile")
fun HomeScreen() {
/** your sweet composable code **/
}
Thats it! After rebuilding the project, several new kotlin files will be generated. Here is a quick description, but if you want to see how it actually looks like, in this particular example, check this gist
Routes.kt
- This file contains all your routes descriptions that can be used to declare the destinations in theNavGraphBuilder
RoutesActions.kt
- This file contains the Nav Actions for your routes. Simillar toNavDirections
fromsafeArg
plugin it will build a proper path only accpeting the arguments you provided in the annotations.*RouteArgs.kt
-> for every route that contains arguments an Argument Wrapper will be generated -DetailsRouteArgs.kt
in our case. This wrapper will also contain several helper functions that will help with arguments handling.
Now lets start using all this code thats been generated, starting with our NavGraphBuilder
. Routes.kt
now contains two objects called home
and path
. They all contain two properties
path
- full path of the route including the arguments we specifednavArgs
- list of theNamedNavArgument
to fully describe our arguments
Lets put them to good use π
NavHost(navController = navController, startDestination = Routes.home.path) { composable(Routes.home.path) { HomeScreen(/*...*/) } composable(Routes.details.path, Routes.details.navArgs) {...}}
Our only transition also needs some love, lets use our RoutesActions.kt
composable(Routes.home.path) { HomeScreen( onClick = { navController.navigate(RoutesActions.toDetails(id = "MyFancyId", number = 0)) // navController.navigate(RoutesActions.toDetails("myId")) in case we don't want to pass the number } ) }
Final touch - the arguments.
/*** Get args from NavBackStackEntry**/composable(/** our details route **/ ) { backStackEntry -> val args = DetailsRouteArgs.fromBackStackEntry(backStackEntry) DetailsScreen(args)}/*** OR get args from SavedStateHandle**/class DetailsScreenViewModel @Inject constructor( savedStateHandle: SavedStateHandle) { init { val args = DetailsRouteArgs.fromSavedStateHandle(savedStateHandle) }}
And thats it!
Thanks for your time! I really enjoyed making this tiny little library and I hope you can find it useful, if so please leave a π! I will really appreciate any feedback and suggestions so feel free to reach out to me π Here is the GitHub page for those of you who are interested.
This phrase looks a bit weird, maybe "In Compose world, of course, we use functions"
@Composable
This doesn't add much to the code
String code has a extra space in the end
spelling
Spelling, should be "Note"
Spelling
Maybe add the param names, in Android Studio it shows so it's fine, but for who is reading they have no idea what the param is supposed to be
spelling
Would be nice if it was broken into lines so it's easier to read than scrolling
Same as above
Same