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!
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.