Reputation: 639
I am working on a project that utilizes Jetpack Compose Navigation with Type-Safe Navigation. Within my application, I have an composable function responsible for hosting the navigation graph. Here's a simplified version of the code:
@Composable
fun Content(
navController: NavHostController = rememberNavController()
) {
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = currentBackStackEntry?.toRoute<Route>()
NavHost(navController = navController, startDestination = Route.Route1){
composable<Route.Route1> { }
composable<Route.Route2> { }
composable<Route.Route3> { }
}
}
@Serializable
sealed interface Route {
@Serializable
data object Route1 : Route
@Serializable
data object Route2 : Route
@Serializable
data object Route3 : Route
}
I'm attempting to retrieve the current route object outside the composable
block: currentBackStackEntry?.toRoute<Route>()
. However, I encounter the following exception:
IllegalArgumentException: Polymorphic value has not been read for class null
It appears that polymorphic behavior is not supported/enabled in this context. Can someone provide guidance on how to solve this issue? I need to be able to obtain the current route object outside the NavHost composable
block using toRoute<Route>
function. Thank you!
Upvotes: 15
Views: 1641
Reputation: 639
An alternative approach to tracking the current route in a Compose navigation setup is to use a mutableState variable that updates each time a new screen enters the composition. Here’s a basic example:
@Composable
fun Content(
navController: NavHostController = rememberNavController()
) {
val startDestination = Route.Route1
var currentRoute: Route by remember { mutableStateOf(startDestination) }
NavHost(navController = navController, startDestination = startDestination) {
composable<Route.Route1> {
LaunchedEffect(Unit) {
currentRoute = it.toRoute<Route.Route1>()
}
}
composable<Route.Route2> {
LaunchedEffect(Unit) {
currentRoute = it.toRoute<Route.Route2>()
}
}
composable<Route.Route3> {
LaunchedEffect(Unit) {
currentRoute = it.toRoute<Route.Route3>()
}
}
}
}
However, since LaunchedEffect creates a coroutine internally to execute potentially suspending functions, it might introduce some overhead and delay. If you’re looking for a more lightweight solution, you can implement a custom effect that avoids this overhead, like so:
import androidx.compose.runtime.Composable
import androidx.compose.runtime.RememberObserver
import androidx.compose.runtime.remember
@Composable
fun InvokedEffect(
key1: Any?,
effect: () -> Unit,
) {
remember(key1) { InvokedEffectImpl(effect) }
}
@Composable
fun InvokedEffect(
key1: Any?,
key2: Any?,
effect: () -> Unit,
) {
remember(key1, key2) { InvokedEffectImpl(effect) }
}
@Composable
fun InvokedEffect(
key1: Any?,
key2: Any?,
key3: Any?,
effect: () -> Unit,
) {
remember(key1, key2, key3) { InvokedEffectImpl(effect) }
}
@Composable
fun InvokedEffect(
vararg keys: Any?,
effect: () -> Unit,
) {
remember(*keys) { InvokedEffectImpl(effect) }
}
internal class InvokedEffectImpl(
private val effect: () -> Unit
) : RememberObserver {
override fun onRemembered() {
effect()
}
override fun onForgotten() {}
override fun onAbandoned() {}
}
After replacing LaunchedEffect with InvokedEffect, your final code would look like this:
@Composable
fun Content(
navController: NavHostController = rememberNavController()
) {
val startDestination = Route.Route1
var currentRoute: Route by remember { mutableStateOf(startDestination) }
NavHost(navController = navController, startDestination = startDestination) {
composable<Route.Route1> {
InvokedEffect(Unit) {
currentRoute = it.toRoute<Route.Route1>()
}
}
composable<Route.Route2> {
InvokedEffect(Unit) {
currentRoute = it.toRoute<Route.Route2>()
}
}
composable<Route.Route3> {
InvokedEffect(Unit) {
currentRoute = it.toRoute<Route.Route3>()
}
}
}
}
This approach provides a more efficient way of tracking the current route. This is what I’m using in my projects.
Upvotes: 2
Reputation: 123
I solved this using Kotlin Reflect, check if this helps you out
@Serializable
sealed class Screen {
companion object {
fun fromRoute(route: String): Screen? {
return Screen::class.sealedSubclasses.firstOrNull {
route.contains(it.qualifiedName.toString())
}.objectInstance
}
}
@Serializable
data object Home : Screen()
@Serializable
data class Detail(val id: Int) : Screen()
}
Make sure you have:
implementation(kotlin("reflect"))
Use case:
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = remember(backStackEntry) {
Screen.fromRoute(backStackEntry?.destination?.route ?: "")
}
EDIT:
If you want to get the arguments as well, I got into this solution:
companion object {
fun fromRoute(route: String, args: Bundle?): Screen? {
val subclass = Screen::class.sealedSubclasses.firstOrNull {
route.contains(it.qualifiedName.toString())
}
return subclass?.let { createInstance(it, args) }
}
private fun <T : Any> createInstance(kClass: KClass<T>, bundle: Bundle?): T? {
val constructor = kClass.primaryConstructor
return constructor?.let {
val args = it.parameters.associateWith { param ->
bundle?.get(param.name)
}
it.callBy(args)
} ?: kClass.objectInstance
}
}
Use Case
val backStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = remember(backStackEntry) {
Screen.fromRoute(
route = backStackEntry?.destination?.route ?: "",
args = backStackEntry?.arguments
)
}
Upvotes: 5
Reputation: 639
I found a workaround, though it requires additional ProGuard configuration and may be error-prone.
First, extend the NavBackStackEntry with a custom toRoute property:
val NavBackStackEntry.toRoute: Route?
get() = destination.route?.let {
when (it.substringBefore("?").substringBefore("/").substringAfterLast(".")) {
Route.Route1::class.simpleName -> toRoute<Route.Route1>()
Route.Route2::class.simpleName -> toRoute<Route.Route2>()
Route.Route3::class.simpleName -> toRoute<Route.Route3>()
else -> throw IllegalArgumentException("Route: $it, is not recognized")
}
}
Next, update your proguard-rules.pro to ensure ProGuard keeps the necessary classes and their names intact:
-keepnames class com.example.Route
-keepnames class * extends com.example.Route
I hope this solution helps, but I am still looking for a direct way to retrieve the current route object without the need for extra workaround code. Any guidance on a more straightforward approach would be greatly appreciated.
Upvotes: 2