Search code examples
android-jetpack-composejson-deserializationandroid-jetpack-compose-navigation

Unable to get route object from currentBackStackEntry in Compose Navigation outside NavHost composable block using toRoute extension


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!


Solution

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