Skip to content

Instantly share code, notes, and snippets.

@jimlyas
Last active October 3, 2024 02:37
Show Gist options
  • Save jimlyas/c5dd09c69066ac8e022558de165ce8b6 to your computer and use it in GitHub Desktop.
Save jimlyas/c5dd09c69066ac8e022558de165ce8b6 to your computer and use it in GitHub Desktop.
reflection-navigation
// 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
)
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"))
[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>()
// 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
}
// 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
}
)
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
)
}
composable<SomeProfile>(
typeMap = mapOf(typeOf<Account>() to NavType.CustomNavType)
) {
// Your composable function
}
@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")
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment