Blog Infos
Author
Published
Topics
, , , ,
Published
Photo by Jordan Madrid on Unsplash
The Past

So if you like me, an Android Developer who uses Jetpack Compose daily, you probably have the same issue as me when using the navigation library.

I’m personally not so fond of the String route basis for the navigation. There’s so much to declare and remember for basic things, like the String route to associate with your Composable function, keys for each argument, and NavType for each argument.

In addition, because your route is a plain String, you need to implement a way to connect your String route to your arguments like storing it in the same class or the same package or encapsulating it in a sealed class.

// Inside NavHost
composable<SomeProfile>(
typeMap = mapOf(
typeOf<Account>() to NavType.CustomNavType
)
) {
// no TypeMap
val arg = it.toRoute<SomeProfile>()
// Your Composable Screen
}
// Inside ViewModel
// TypeMap is defined again
val arg = savedStateHandle.toRoute<SomeProfile>(
typeOf<Account>() to NavType.CustomNavType
)
view raw arg.kt hosted with ❤ by GitHub
dependencies {
implementation(libs.compose.navigation)
implementation(libs.gson)
implementation(kotlin("reflect"))
}
inline fun <reified destination : Any> SavedStateHandle.getArg(): destination? = try {
destination::class.primaryConstructor?.callBy(
destination::class.primaryConstructor?.parameters?.associate { parameter ->
parameter to parameter.getValueFrom(this)
}.orEmpty()
)
} catch (t: Throwable) {
null
}
inline fun <reified destinationClass : Any> NavBackStackEntry.getArg(): destinationClass? = try {
destinationClass::class.primaryConstructor?.callBy(
destinationClass::class.primaryConstructor?.parameters?.associate { parameter ->
parameter to parameter.getValueFrom(this.arguments ?: Bundle())
}.orEmpty()
)
} catch (t: Throwable) {
null
}
// ** Register route to NavGraph
// Before
composable(
route = "somehow.this.works",
arguments = listOf(
navArgument("id") {
type = NavType.IntType
nullable = false
},
navArgument("name") {
type = NavType.StringType
nullable = false
}
)
) {
// Your composable
}
//After
@Serializer
data class Profile(val id : int, val name : String)
composable<Profile> {
// Your composable
}
// ** Navigating
// Before
navController.navigate("somehow.this.works?id=1&name=Something")
// After
navController.navigate(Profile(1, "Something"))
view raw difference.kt hosted with ❤ by GitHub
[versions]
compose-navigation = "2.8.0"
gson = "2.10.1"
kotlin = "1.9.10"
reflect = "0.1.0"
[libraries]
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "compose-navigation" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
reflect-nav = { group = "io.github.jimlyas", name = "reflection-navigation", version.ref = "reflect" }
fun <destination : Any> NavController.navigateTo(
route: destination,
navOptions: NavOptions? = null,
navExtras: Extras? = null
) {
val kClass = route::class
val intendedUri = Uri.Builder()
.scheme(NAVIGATION_SCHEME)
.authority(NAVIGATION_AUTHORITY)
.path(kClass.asRouteName())
kClass
.declaredMemberProperties
.forEach { property ->
property.isAccessible = true
val name = property.name
val value = property.getter.call(route)
value?.let {
intendedUri.appendQueryParameter(
name, value.parseToString()
)
}
}
navigate(
request = NavDeepLinkRequest.Builder.fromUri(intendedUri.build()).build(),
navOptions = navOptions,
navigatorExtras = navExtras
)
}
// In NavHost
composeRoute<SomeProfile> {
val args = it.getArg<SomeProfile>()
}
// In ViewModel
val args = savedStateHandler.getArg<SomeProfile>()
view raw newarg.kt hosted with ❤ by GitHub
// Navigating to other screen
navController.navigateTo(
// SomeProfile instance
)
@ReflectiveRoute
data class SomeProfile(val id: Int, val name: String, val account: Account)
data class Account(val currency: String, val balance: BigDecimal)
// Inside NavHost
composeRoute<SomeProfile> {
// Your Composable Screen
}
view raw newroute.kt hosted with ❤ by GitHub
// This is your route
val route = "somehow.this.works"
// your Arguments
val param1 = "yourParam"
val param2 = "yourOtherParam"
// need to declare your NavType for each arguments
// need to declare if the param nullable
val arguments = listOf(
navArgument(param1) {
type = NavType.StringType
nullable = true
},
navArgument(param2) {
type = NavType.IntType
nullable = false
}
)
view raw route.kt hosted with ❤ by GitHub
inline fun <reified routeClass : Any> NavGraphBuilder.composeRoute(
deepLinks: List<NavDeepLink> = emptyList(),
noinline enterTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
EnterTransition?)? = null,
noinline exitTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
ExitTransition?)? = null,
noinline popEnterTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
EnterTransition?)? = enterTransition,
noinline popExitTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
ExitTransition?)? = exitTransition,
noinline sizeTransform:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
SizeTransform?)? = null,
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
val routeKClass = routeClass::class
val args = routeKClass.primaryConstructor?.parameters?.map { param ->
navArgument(param.name.orEmpty()) {
type = param.toNavType()
nullable = param.type.isMarkedNullable
}
}.orEmpty()
val routeName = buildString {
append(routeKClass.asRouteName())
append(QUESTION_MARK)
args.map { it.name }.forEach { s -> append("$s={$s}&") }
deleteAt(lastIndex)
}
composable(
route = routeName,
arguments = args,
deepLinks = deepLinks,
enterTransition = enterTransition,
exitTransition = exitTransition,
popEnterTransition = popEnterTransition,
popExitTransition = popExitTransition,
sizeTransform = sizeTransform,
content = content
)
}
view raw routext.kt hosted with ❤ by GitHub
composable<SomeProfile>(
typeMap = mapOf(typeOf<Account>() to NavType.CustomNavType)
) {
// Your composable function
}
view raw typemap.kt hosted with ❤ by GitHub
@Serializable
data class SomeProfile(val id: Int, val name: String, val account: Account)
@Serializable
data class Account(val currency: String, val balance: @Contextual BigDecimal)
fun NavGraphBuilder.someRoute() {
composable<SomeProfile> { SomeScreen() }
}
@Composable
internal fun SomeScreen() {
Text(text = "Still Trying")
}
view raw unsupported.kt hosted with ❤ by GitHub
Route and Arguments in Compose Navigation

Furthermore, parameters are now will be parsed as String because, in a String-based route, all navigation arguments are treated as query parameters or paths like in a URI.

So when the new Type Safety was introduced in Compose Navigation, I was so thrilled to try it out.

The Present

In the new Type Safe navigation, The underlying implementation of the String-based route is still the same. But using the power of Serializer, they make it less painful than before.

Instead of registering your route as a String and using said String when navigating to another Screen, you now use Object/Class as your route. And when you need to navigate to that Screen you need to pass the instance.

// Inside NavHost
composable<SomeProfile>(
typeMap = mapOf(
typeOf<Account>() to NavType.CustomNavType
)
) {
// no TypeMap
val arg = it.toRoute<SomeProfile>()
// Your Composable Screen
}
// Inside ViewModel
// TypeMap is defined again
val arg = savedStateHandle.toRoute<SomeProfile>(
typeOf<Account>() to NavType.CustomNavType
)
view raw arg.kt hosted with ❤ by GitHub
dependencies {
implementation(libs.compose.navigation)
implementation(libs.gson)
implementation(kotlin("reflect"))
}
inline fun <reified destination : Any> SavedStateHandle.getArg(): destination? = try {
destination::class.primaryConstructor?.callBy(
destination::class.primaryConstructor?.parameters?.associate { parameter ->
parameter to parameter.getValueFrom(this)
}.orEmpty()
)
} catch (t: Throwable) {
null
}
inline fun <reified destinationClass : Any> NavBackStackEntry.getArg(): destinationClass? = try {
destinationClass::class.primaryConstructor?.callBy(
destinationClass::class.primaryConstructor?.parameters?.associate { parameter ->
parameter to parameter.getValueFrom(this.arguments ?: Bundle())
}.orEmpty()
)
} catch (t: Throwable) {
null
}
// ** Register route to NavGraph
// Before
composable(
route = "somehow.this.works",
arguments = listOf(
navArgument("id") {
type = NavType.IntType
nullable = false
},
navArgument("name") {
type = NavType.StringType
nullable = false
}
)
) {
// Your composable
}
//After
@Serializer
data class Profile(val id : int, val name : String)
composable<Profile> {
// Your composable
}
// ** Navigating
// Before
navController.navigate("somehow.this.works?id=1&name=Something")
// After
navController.navigate(Profile(1, "Something"))
view raw difference.kt hosted with ❤ by GitHub
[versions]
compose-navigation = "2.8.0"
gson = "2.10.1"
kotlin = "1.9.10"
reflect = "0.1.0"
[libraries]
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "compose-navigation" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
reflect-nav = { group = "io.github.jimlyas", name = "reflection-navigation", version.ref = "reflect" }
fun <destination : Any> NavController.navigateTo(
route: destination,
navOptions: NavOptions? = null,
navExtras: Extras? = null
) {
val kClass = route::class
val intendedUri = Uri.Builder()
.scheme(NAVIGATION_SCHEME)
.authority(NAVIGATION_AUTHORITY)
.path(kClass.asRouteName())
kClass
.declaredMemberProperties
.forEach { property ->
property.isAccessible = true
val name = property.name
val value = property.getter.call(route)
value?.let {
intendedUri.appendQueryParameter(
name, value.parseToString()
)
}
}
navigate(
request = NavDeepLinkRequest.Builder.fromUri(intendedUri.build()).build(),
navOptions = navOptions,
navigatorExtras = navExtras
)
}
// In NavHost
composeRoute<SomeProfile> {
val args = it.getArg<SomeProfile>()
}
// In ViewModel
val args = savedStateHandler.getArg<SomeProfile>()
view raw newarg.kt hosted with ❤ by GitHub
// Navigating to other screen
navController.navigateTo(
// SomeProfile instance
)
@ReflectiveRoute
data class SomeProfile(val id: Int, val name: String, val account: Account)
data class Account(val currency: String, val balance: BigDecimal)
// Inside NavHost
composeRoute<SomeProfile> {
// Your Composable Screen
}
view raw newroute.kt hosted with ❤ by GitHub
// This is your route
val route = "somehow.this.works"
// your Arguments
val param1 = "yourParam"
val param2 = "yourOtherParam"
// need to declare your NavType for each arguments
// need to declare if the param nullable
val arguments = listOf(
navArgument(param1) {
type = NavType.StringType
nullable = true
},
navArgument(param2) {
type = NavType.IntType
nullable = false
}
)
view raw route.kt hosted with ❤ by GitHub
inline fun <reified routeClass : Any> NavGraphBuilder.composeRoute(
deepLinks: List<NavDeepLink> = emptyList(),
noinline enterTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
EnterTransition?)? = null,
noinline exitTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
ExitTransition?)? = null,
noinline popEnterTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
EnterTransition?)? = enterTransition,
noinline popExitTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
ExitTransition?)? = exitTransition,
noinline sizeTransform:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
SizeTransform?)? = null,
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
val routeKClass = routeClass::class
val args = routeKClass.primaryConstructor?.parameters?.map { param ->
navArgument(param.name.orEmpty()) {
type = param.toNavType()
nullable = param.type.isMarkedNullable
}
}.orEmpty()
val routeName = buildString {
append(routeKClass.asRouteName())
append(QUESTION_MARK)
args.map { it.name }.forEach { s -> append("$s={$s}&") }
deleteAt(lastIndex)
}
composable(
route = routeName,
arguments = args,
deepLinks = deepLinks,
enterTransition = enterTransition,
exitTransition = exitTransition,
popEnterTransition = popEnterTransition,
popExitTransition = popExitTransition,
sizeTransform = sizeTransform,
content = content
)
}
view raw routext.kt hosted with ❤ by GitHub
composable<SomeProfile>(
typeMap = mapOf(typeOf<Account>() to NavType.CustomNavType)
) {
// Your composable function
}
view raw typemap.kt hosted with ❤ by GitHub
@Serializable
data class SomeProfile(val id: Int, val name: String, val account: Account)
@Serializable
data class Account(val currency: String, val balance: @Contextual BigDecimal)
fun NavGraphBuilder.someRoute() {
composable<SomeProfile> { SomeScreen() }
}
@Composable
internal fun SomeScreen() {
Text(text = "Still Trying")
}
view raw unsupported.kt hosted with ❤ by GitHub
The Old and The New way in Compose Navigation

Everything is all and well in the world, or is it?

If all of your arguments are primitive types like String, int, Boolean, etc… then you can relax and use the latest compose navigation version. But if not, perhaps you put third-party classes as your navigation argument like BigDecimal or UUID then we need to talk.

It is considered bad practice to include complex data as an argument, as mentioned here:

from Developer Android Documentation

 

But sometimes we can’t help it, either the application flow forces us to pass some data (or even a list of objects) or we are just lazy, so sometimes we want to pass the “unsupported parameter” type as arguments. But how to do that in the latest navigation compose library?

Let’s say we have a code like this:

// Inside NavHost
composable<SomeProfile>(
typeMap = mapOf(
typeOf<Account>() to NavType.CustomNavType
)
) {
// no TypeMap
val arg = it.toRoute<SomeProfile>()
// Your Composable Screen
}
// Inside ViewModel
// TypeMap is defined again
val arg = savedStateHandle.toRoute<SomeProfile>(
typeOf<Account>() to NavType.CustomNavType
)
view raw arg.kt hosted with ❤ by GitHub
dependencies {
implementation(libs.compose.navigation)
implementation(libs.gson)
implementation(kotlin("reflect"))
}
inline fun <reified destination : Any> SavedStateHandle.getArg(): destination? = try {
destination::class.primaryConstructor?.callBy(
destination::class.primaryConstructor?.parameters?.associate { parameter ->
parameter to parameter.getValueFrom(this)
}.orEmpty()
)
} catch (t: Throwable) {
null
}
inline fun <reified destinationClass : Any> NavBackStackEntry.getArg(): destinationClass? = try {
destinationClass::class.primaryConstructor?.callBy(
destinationClass::class.primaryConstructor?.parameters?.associate { parameter ->
parameter to parameter.getValueFrom(this.arguments ?: Bundle())
}.orEmpty()
)
} catch (t: Throwable) {
null
}
// ** Register route to NavGraph
// Before
composable(
route = "somehow.this.works",
arguments = listOf(
navArgument("id") {
type = NavType.IntType
nullable = false
},
navArgument("name") {
type = NavType.StringType
nullable = false
}
)
) {
// Your composable
}
//After
@Serializer
data class Profile(val id : int, val name : String)
composable<Profile> {
// Your composable
}
// ** Navigating
// Before
navController.navigate("somehow.this.works?id=1&name=Something")
// After
navController.navigate(Profile(1, "Something"))
view raw difference.kt hosted with ❤ by GitHub
[versions]
compose-navigation = "2.8.0"
gson = "2.10.1"
kotlin = "1.9.10"
reflect = "0.1.0"
[libraries]
compose-navigation = { group = "androidx.navigation", name = "navigation-compose", version.ref = "compose-navigation" }
gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" }
reflect-nav = { group = "io.github.jimlyas", name = "reflection-navigation", version.ref = "reflect" }
fun <destination : Any> NavController.navigateTo(
route: destination,
navOptions: NavOptions? = null,
navExtras: Extras? = null
) {
val kClass = route::class
val intendedUri = Uri.Builder()
.scheme(NAVIGATION_SCHEME)
.authority(NAVIGATION_AUTHORITY)
.path(kClass.asRouteName())
kClass
.declaredMemberProperties
.forEach { property ->
property.isAccessible = true
val name = property.name
val value = property.getter.call(route)
value?.let {
intendedUri.appendQueryParameter(
name, value.parseToString()
)
}
}
navigate(
request = NavDeepLinkRequest.Builder.fromUri(intendedUri.build()).build(),
navOptions = navOptions,
navigatorExtras = navExtras
)
}
// In NavHost
composeRoute<SomeProfile> {
val args = it.getArg<SomeProfile>()
}
// In ViewModel
val args = savedStateHandler.getArg<SomeProfile>()
view raw newarg.kt hosted with ❤ by GitHub
// Navigating to other screen
navController.navigateTo(
// SomeProfile instance
)
@ReflectiveRoute
data class SomeProfile(val id: Int, val name: String, val account: Account)
data class Account(val currency: String, val balance: BigDecimal)
// Inside NavHost
composeRoute<SomeProfile> {
// Your Composable Screen
}
view raw newroute.kt hosted with ❤ by GitHub
// This is your route
val route = "somehow.this.works"
// your Arguments
val param1 = "yourParam"
val param2 = "yourOtherParam"
// need to declare your NavType for each arguments
// need to declare if the param nullable
val arguments = listOf(
navArgument(param1) {
type = NavType.StringType
nullable = true
},
navArgument(param2) {
type = NavType.IntType
nullable = false
}
)
view raw route.kt hosted with ❤ by GitHub
inline fun <reified routeClass : Any> NavGraphBuilder.composeRoute(
deepLinks: List<NavDeepLink> = emptyList(),
noinline enterTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
EnterTransition?)? = null,
noinline exitTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
ExitTransition?)? = null,
noinline popEnterTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
EnterTransition?)? = enterTransition,
noinline popExitTransition:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
ExitTransition?)? = exitTransition,
noinline sizeTransform:
(AnimatedContentTransitionScope<NavBackStackEntry>.() -> @JvmSuppressWildcards
SizeTransform?)? = null,
noinline content: @Composable AnimatedContentScope.(NavBackStackEntry) -> Unit
) {
val routeKClass = routeClass::class
val args = routeKClass.primaryConstructor?.parameters?.map { param ->
navArgument(param.name.orEmpty()) {
type = param.toNavType()
nullable = param.type.isMarkedNullable
}
}.orEmpty()
val routeName = buildString {
append(routeKClass.asRouteName())
append(QUESTION_MARK)
args.map { it.name }.forEach { s -> append("$s={$s}&") }
deleteAt(lastIndex)
}
composable(
route = routeName,
arguments = args,
deepLinks = deepLinks,
enterTransition = enterTransition,
exitTransition = exitTransition,
popEnterTransition = popEnterTransition,
popExitTransition = popExitTransition,
sizeTransform = sizeTransform,
content = content
)
}
view raw routext.kt hosted with ❤ by GitHub
composable<SomeProfile>(
typeMap = mapOf(typeOf<Account>() to NavType.CustomNavType)
) {
// Your composable function
}
view raw typemap.kt hosted with ❤ by GitHub
@Serializable
data class SomeProfile(val id: Int, val name: String, val account: Account)
@Serializable
data class Account(val currency: String, val balance: @Contextual BigDecimal)
fun NavGraphBuilder.someRoute() {
composable<SomeProfile> { SomeScreen() }
}
@Composable
internal fun SomeScreen() {
Text(text = "Still Trying")
}
view raw unsupported.kt hosted with ❤ by GitHub

In addition to adding annotation @contextual annotation to the BigDecimal, we need to add @Serializable annotation too to the Account class. What happened when we built this code?

IllegalStateArgumentException

 

Serializer still can’t parse the Account class as an argument, because we still have to add Custom NavType when declaring it from the composable function. So it will look something like this:

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filetypemapkt

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

No results found.

Jobs

You can look on the internet what the CustomNavType will be for your code, or use generic to handle it dynamically. You’ll need to add custom this NavType for the custom class.

In addition, it still can’t provide mapping for classes that we can’t add the @serializer annotation to, like BigDecimal. Here’s the link to the issueTracker:

https://issuetracker.google.com/issues/348468840?source=post_page—–4c5b565f660f——————————–

I don’t think there’s a workaround for that right now, so you will have to wait until the library supports it natively.

The other thing is collecting the navigation arguments can be done in two ways:

  • From NavBackStackEntry from calling the composable function in NavGraphBuilder
  • From SavedStateHandle from ViewModel

These TypeMaps will be used to parse your argument from String to intended type when called from NavBackStackEntry. On the other hand, when using SavedStateHandle, you need to pass the TypeMap again somehow.

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6fileargkt

Perhaps there might be a good reason why TypeMap still needs to be re-defined from ViewModel, or maybe the SavedStateHandle can’t configure which NavType to use from the given route, or it just simply can’t access it from the current implementation.

And just like before, there’s currently no workaround if you don’t want to do that, you will have to pass your NavType again if you’re accessing your arguments from SavedStateHandle.

or is it?

The idea

I usually use my arguments from ViewModel and I often pass complex data as arguments, so with the latest composable navigation library I have two issues:

  • Currently, Serializer can’t use third-party classes as arguments
  • I’m just too lazy to be registering NavType every time I register my route and when I want to access my arguments in ViewModel

I want to make new type of abstraction or extension of the current API to fulfill my needs. I’m fully aware there are some libraries that can do that for me already, but I want to see what I’m able to make.

I want to be able to make the implementation as close as possible to the official compose navigation library but with my own spin. So this is what I came up with:

Some Idea I’m cooking

 

Using Kotlin Reflection, Extension Function, and GSON, it is possible to do all that.

The Implementation

So first let’s add the dependencies what we will be using like this:

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filelibscatalogtoml

And adding it to the build.gradle.kts file like this:

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filebuildgradlekts

Now we’re all set, let’s get cooking.

For dong this, we’re going to use the old plain String-based route because there’s not much we can do with the latest implementation that uses Type-Safety and Serializer.

First, let’s make the extension function to declare our route:

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6fileroutextkt

From the generic type, we call :class to get the KClass that will be used for creating the route name and getting all the parameters of the KClass and registering it as the navigation arguments.

I usedprimaryConstructor.parameters to access all the parameters for the constructor in the type of KParameter. From the KParameter instance, we will use the parameter name that is registered in the KClass as the navigation argument key. Another thing we’re going to do is define the NavType from KParameter.type to know which NavType is compatible with the parameter type, and for the nullability, we can easily call Kparameter.type.isMarkedNullable to know if the parameter is nullable and the navigation argument will reflect that.

After getting all the navigation argument types, It’s time to define the route for the composable. Using buildString, we will add:

  • The qualified name of the KClass, and shorten it to only contain three words using the asRouteName function
  • Add all the arguments from earlier as URI parameter type

It will look something like this:

package.name.ClassName?param1={param1}&param2={param2}

Next, we define code for navigating to another Screen:

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filenavigationextkt

From the generic type, we call :class to get the KClass type of route parameter.

I used URI builder to help add more parameters for the URI that will be passed to the navigation function. For the URI, I added the qualified name from KClass like before using the asRouteName function.

declaredMemberProperties will give us the parameters that we will add to the URI. Looping the parameters and accessing the parameter name and its value before adding it to the URI builder earlier.

The result URI will look like this:

android-app://androidx.navigation/package.name.ClassName?param1=param1Value&param2=param2Value

This is basically the same URI that will be passed when using the String-type route

Lastly, for accessing the navigation argument the code will look like this:

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filedestinationextkt

There are two extension functions from NavBackStackEntry and SavedStateHandle with the name getArg for simplicity. With Kotlin Reflection, it will call the primary constructor of KClass from generic.

But before calling the primary constructor, we’re going to need all the primary constructor’s parameters that we will get from the receiver of the extension function.

This code will initialize the given generic type and return the instance.

The Result

By making a new function composeRoute, I was able to make an abstraction of the old plain String as a route so the consumer no longer needs to do all the additional stuff that you need to do earlier.

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filenewroutekt

Please don’t give too much attention to the annotation, it doesn’t really do much

Navigating to another screen will look the same as the official compose navigation library.

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filenewnavigationkt

Whether you’re accessing your navigation arguments from NavBackStackEntry or SavedStateHandle, you’ll only need to call the getArg function and define which route you want to access.

https://gist.github.com/c5dd09c69066ac8e022558de165ce8b6filenewargkt

Using these three extensions functions, you’ll have more or less similar experience as when using the official library. In addition whether your navigation uses all primitive data types or not, nevertheless you can use this out of the box.

If you’re interested or want to deep dive more to the code, I made this as a library here:

https://github.com/jimlyas/reflection-navigation?source=post_page—–4c5b565f660f——————————–

Please do check it out or try it out, and give me your comments on what do you think.

Thank for reading.

This article is previously published on proandroiddev.com

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu