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

3

9

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!

Anabolite answered 16/5, 2024 at 12:22 Comment(0)
A
0

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.

Anabolite answered 25/8, 2024 at 11:40 Comment(0)
T
5

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
    )
}
Triploid answered 7/6, 2024 at 23:21 Comment(1)
Very 👍 solution but it would be greater if we can get args from the route itself. Also in a release version, due to R8, the route string keeps the original class names. Which forces us to defines proguard entries.Anabolite
A
2

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.

Anabolite answered 17/5, 2024 at 12:0 Comment(0)
A
0

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.

Anabolite answered 25/8, 2024 at 11:40 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.