Safe Arguments Generator
Yet another attempt to add safe arguments to Compose Navigation.
Why
Since routes in Navigation Component don't support safe arguments out of the box as well as require a lot of boilerplate code, this library was meant to be made.
The main focus of the library is a simplified approach for declaring routes and arguments. What's more, this library doesn't force you to declare your screen composables in any particular way, which was a deal-breaker in several other existing safe-args projects.
Ok, show me the code
You declare all routes within a single interface like this:
@GenerateRoutes("Routes")
interface RouteActions {
fun toMainScreen(): String
fun toSecondScreen(id: Int): String
// ...
}
The processor then generates a class with the name Routes
as specified in the annotation, which can be used to declare your navigation within NavHost
:
NavHost(navController, startDestination = Routes.toMainScreen()) {
composable(Routes.MainScreen) {
// your composable UI goes here
}
composableWithArgs(Routes.SecondScreen) { args ->
// here you can use args.id
}
// ...
}
And in order to navigate between destinations, you just call:
navController.navigate(Routes.toSecondScreen(id = 123))
As simple as that.
There is of course a bunch of other useful features that will be explained further. But first...
Add the library to your project
First, add Kotlin Symbol Processing plugin to the module of your project where you are going to declare routes:
plugins {
// ...
id("com.google.devtools.ksp") version "1.5.31-1.0.1"
}
Then add dependencies:
dependencies {
// ...
ksp("dev.olshevski.safeargs:ksp:1.0.0")
implementation("dev.olshevski.safeargs:api-compose:1.0.0")
}
In order for the project to discover newly generated files, add build/generated/ksp/...
folders to the source sets like this:
android {
// ...
applicationVariants.all {
val variantName = name
sourceSets {
named(variantName) {
kotlin.srcDir(file("build/generated/ksp/$variantName/kotlin"))
}
}
}
}
}
And that's it.
Alternative dependencies
If you for some reason want to use this library in a non-Compose application, or you just want to write your own custom NavGraphBuilder
extensions you can use:
implementation("dev.olshevski.safeargs:api:1.0.0")
instead of api-compose
. The api
artifact contains only essential declarations without any Compose dependencies.
API
Declaring routes
You must declare routes inside an interface. The name of the interface is arbitrary. As it declares navigation actions, a good choice for the name may be RouteActions
or similar one.
Inside the interface you declare methods starting with to
prefix and returning String
. Names of methods minus to
prefix will become names of routes. For example, fun toMainScreen(): String
will be interpreted as MainScreen
route.
Every method may contain parameters of types Int
, Long
, Float
, Boolean
, String
or String?
. This is the limitation of Navigation Component, see Supported Argument Types.
Default values for parameters may be specified as well.
Then you annotate the interface with @GenerateRoutes
specifying the name of the generated class, e.g. "Routes"
. This class will inherit the interface and provide implementations for every declared method. Every method will then be able to build a String
representation of a route with arguments applied. Thus the required String
return value.
For method toSecondScreen
from the example above, the generated implementation will be:
override fun toSecondScreen(id: Int): String = "Routes_SecondScreen/$id"
Note that there is no limitation on the number of GenerateRoutes
-annotated interfaces. Feel free to group and organize your routes however you want. The only requirement: no duplicate names for generated classes within the same namespace.
What else is generated
The route declarations themselves are constructed. They all inherit the base Route
class and provide the pattern
property and the list of NamedNavArguments
which are both required to declare navigation routes within NavHost
.
And of course, every generated route with parameters contains Args
data class.
Let's see what a single declaration of fun toSecondScreen(id: Int): String
generates:
object Second : Route<Second.Args>(
"Routes_Second/{id}", listOf(
navArgument(Args.Id) {
type = NavType.IntType
},
)
) {
override fun argsFrom(bundle: Bundle) = Args.from(bundle)
override fun argsFrom(savedStateHandle: SavedStateHandle) =
Args.from(savedStateHandle)
data class Args(
val id: Int
) {
companion object {
const val Id: String = "id"
fun from(bundle: Bundle) = Args(
bundle.getInt(Id),
)
fun from(savedStateHandle: SavedStateHandle) = Args(
savedStateHandle[Id]!!,
)
}
}
}
As you can see, a bunch of useful from
methods are generated and constants for arguments. You may use them as you want. For example, argument constants may be useful for declaring deep-links.
Note: constructing arguments from SavedStateHandle
is useful in ViewModels
. For acquiring arguments from NavBackStackEntry
use argsFrom(navBackStackEntry.arguments)
. navBackStackEntry.savedStateHandle
may simply be null
.
Nested navigation
This library was created with nested navigation in mind. To organize your routes hierarchically, you can simply declare more nested interfaces with @GenerateRoutes
annotation:
@GenerateRoutes("Routes")
interface RouteActions {
// ...
@GenerateRoutes("Subroutes")
interface SubrouteActions {
fun toFirstScreen(): String
fun toSecondScreen(): String
}
}
This declaration will simply be treated as another group of routes, which are placed inside Routes.Subroutes
object. In order for Routes.Subroutes
to be a route itself, you need to add a navigation declaration with the same name:
@GenerateRoutes("Routes")
interface RouteActions {
// ...
fun toSubroutes(): String
@GenerateRoutes("Subroutes")
interface SubrouteActions {
fun toFirstScreen(): String
fun toSecondScreen(): String
}
}
Now Routes.Subroutes
is both a Route
and a container for nested routes.
Extension methods
api
artifacts adds a single extension method for now:
fun NavController.getBackStackEntry(route: Route<*>)
api-compose
artifact adds convenient NavGraphBuilder
extensions for using generated routes and easy acquiring of Args
classes:
NavHost(navController, startDestination = Routes.toMainScreen()) {
composable(Routes.MainScreen) {
// you may get args manually here or in ViewModel
}
composableWithArgs(Routes.SecondScreen) { args ->
// Args are acquired.
// Works well even if route doesn't have any parameters.
}
// same for Dialogs
dialog(Routes.SomeDialog) { /* ... */ }
dialogWithArgs(Routes.AnotherDialog) { args -> /* ... */ }
// nested navigation
navigation(Routes.Subroutes) { /* ... */ }
}
Sample
Please explore the sample
module within the project for better understanding of capabilities of the library.