Jetpack Compose — Simplifying arguments in Navigation
How not to get lost among the Navigation Compose arguments?
There are too many articles related to Navigation (and they are very good), in them they explain how to navigate between “screens” in a simple way, and it is obvious that it is simple as long as the routes are simple, that is, without parameters.
It is easy not to get lost as long as the routes are without parameters
composable("splash_route") { ... }composable("dashboard_route") { ... }
But…
What happens when a real app needs to pass arguments? How can you add them? How can you NOT get lost, confused or forget them?
This example shows how it can be to create a route with arguments:
composable("validate_login/{userId}/{encryptedPassword}/{tokenId}") { ... }composable("create_payment/{userId}/{paymentType}/{currency}/{amount}/{etc}") { ... }// With variablesconst val MAINSCREEN = "mainscreen"
const val FIRST_PARAMETERS_SCREEN = "parameter_screen_1"
const val SECOND_PARAMETERS_SCREEN = "parameter_screen_2""$MAINSCREEN/{$FIRST_PARAMETERS_SCREEN}/{SECOND_PARAMETERS_SCREEN}"
I know I’m not the only one who got lost 😅 so, using the goodness of Kotlin, let’s create a little code that allows us not to get lost among so many arguments.
Let’s get to it!
We need to create a class that allows us to generate the routes, as destinations and as navigation.
This builder class will help us to better understand what we are adding as Destination, parameters and the order in which they are added (as it is very important to remember).
The NavigationItems.Items class will help us define our available destinations during this flow.
You can add as many as your application and flow need.
The Builder class contains the following:
- destination: which is the header of our route, the “ID” that the new view we want to show.
- arguments: in this case it is a private variable, so we can create it depending on the situation (argument name, argument value).
- addArgumentParm(…): As we well know, we have to specify the name of our argument inside “{}”, so this function will help us to add as many as we want and it is here where we can respect an order and ease of reading.
- addArgumentValue(…): like the previous function, we will be able to add as many as we need, however here is the value that we will pass as argument and this no longer takes “{}”, but the simple value that we will pass.
- build(): finally the very powerful method that builds our NavigationItems class.
But now, how can we simplify the creation of this Builder without having to create the class or call all the methods?
Here we all thank the Kotlin gods for such beautiful functions.
This ```typealias NavigateBuilder``` is optional because it will help us to shorten ```NavigationItems.Builder.() -> Unit``` which is really the parameter we need and will help us to create our NavigationItems using this lamda.
The typealias will help us to give a better reading to our code, simplifying the parameters of the following functions.
In the same way the NavigateBuilder.fullRoute extension will only help us to add one more property, which actually creates our NavigationItems class through the builder and returns us the route variable that already contains all the formatting we need.
The ```navigationRoute(…)``` function will allow us to create the navigation route using builder lambdas.
Finally and more important, we will add a function by means of extensions to the NavController class, which will make a bridge between the extensions and typealias that we have just explained, in addition to make the navigation corresponding to the route that we pass as parameter.
Ready, we have our code ready to generate our routes in a different way. And how does it work?
// Declaration of Splash route without parameters
val splashDestination = navigationRoute {
destination = NavigationItems.Item.SPLASH
}
We will be able to create routes without defined parameters, only the route, a simple String. At this point we see no improvement, since it is a simple String…
When we have parameters to transfer is where the routes become complicated, we can pass from this
composable("create_payment/{userId}/{paymentType}/{currency}/{amount}/{etc}") { ... }
to this code
val createPaymentScreenDestination = navigationRoute {
destination = NavigationItems.Item.CREATE_PAYMENT
addArgumentParm("userId")
addArgumentParm("paymentType")
addArgumentParm("currency")
addArgumentParm("amount")
}// output"CREATE_PAYMENT/{userId}/{paymentType}/{currency}/{amount}"
But leaving sentimentality aside, let’s move on to the real example and take some inspiration from:
we will create our class that will separate the actions allowed within the app
Obviously as parameter we will need our NavHostController, with which we will make the navigations defined in the following methods.
showError(…) is a very simple example, we pass what we need as parameter (title, message) and that’ s it, the extensions that we have will help us to generate the destination with arguments by means of the lambda of route, where we add the destination item, a title and a message. And yes, that’s how simple and readable our code is.
All integrated works as follows
- actions will provide us the routes, either without or with parameters.
- We can pass our previously defined route in the following way into the Host.
startDestination = splashDestination
- Inside our composable we add our route and at the end, within each view created we can add specific actions, such as at the end of the Splash we will perform a sample navigation to the Error screen using the simple call:
actions.showError(“Error”, “Unexpected error”, “failed”)
And like the architectures, this small code is only a proposal (a small contribution), there will be thousands of ways to do it, but this has worked for me and has helped me not to get lost in the way of the arguments.