I'm trying to use TypeSafe navigation arguments for my app, but i have issue with serialization.
I made sealed interface which contains all data classes for all screens within my app.
It looks like this:
sealed interface RouteData {
val navigationVisible: Boolean
@Serializable
data class SplashScreenData(
override val navigationVisible: Boolean = false
) : RouteData
@Serializable
data class FirstScreenData(
override val navigationVisible: Boolean = true
) : RouteData
@Serializable
data class SecondScreenData(
override val navigationVisible: Boolean = false,
val id: Int,
val name: String,
) : RouteData
@Serializable
data class ThirdScreenData(
override val navigationVisible: Boolean = false,
val item: String,
) : RouteData
...
}
And now I have NavigationBar which is listening to backStackEntry to collect data:
navController.currentBackStackEntryFlow.collect { backStackEntry ->
val data = try {
backStackEntry.toRoute<RouteData>()
} catch (e: Exception) {
Log.e("TabBar", "Error deserializing RouteData", e)
null
}
data?.let {
isTabBarVisible = it.navigationVisible
}
}
Issue is that I'm getting deserialization exception:
java.lang.IllegalArgumentException: Polymorphic value has not been read for class null
This approach is not supported - only concrete classes can be used with Navigation's APIs (e.g., composable
or toRoute
) as only those classes provide enough information via Serialization.
Instead, you should separate the metadata about destinations from the data classes themselves. Besides being much, much more efficient in how savedInstanceState is being used (e.g., your navigationVisible
is being stored in the Bundle for every instance), this also provides a separation of concerns between static data and data specific to an instance of your data class. This also encourages you to not use these kind of sealed interfaces, which do not scale as you split your destinations across multiple separate modules.
So instead, your destinations can look like:
@Serializable
data object SplashScreenData
@Serializable
data object FirstScreenData
@Serializable
data class SecondScreenData(
val id: Int,
val name: String,
)
@Serializable
data class ThirdScreenData(
val item: String,
)
And you can centralize the logic around bottom nav visibility specifically around the place where you actually use that data - alongside your listener which can then use the hasRoute
method to determine exactly what destination you are on:
navController.currentBackStackEntryFlow.collect { backStackEntry ->
isTabBarVisible = backStackEntry.destination.hasRoute<FirstScreenData>()
}
Of course, if your isTabBarVisible is Compose State that you use to show and hide a bottom bar, you shouldn't be using currentBackStackEntryFlow
at all (since that adds a one frame delay between collecting the value and the recomposition), so you'd instead want to read the state directly:
val currentBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = currentBackStackEntry?.destination
val isTabBarVisible = currentDestination?.hasRoute<FirstScreenData>() == true
Of course, you can use the KClass version as well if you want to keep a set of classes:
val bottomBarVisibleDestinations = listOf(FirstSreenData::class)
val isTabBarVisible = bottomBarVisibleDestinations.any {
currentDestination?.hasRoute(it) == true
}