I had an app made with jetpack compose that worked fine until I upgraded the compose navigation library
from version 2.4.0-alpha07 to version 2.4.0-alpha08
In the alpha08 version it seems to me that the arguments
attribute of the NavBackStackEntry
class is a val
, so it can't be reassigned as we did in the 2.4.0-alpha07 version.
How to solve this problem in version 2.4.0-alpha08?
My navigation component is this:
@Composable
private fun NavigationComponent(navController: NavHostController) {
NavHost(navController = navController, startDestination = "home") {
composable("home") { HomeScreen(navController) }
composable("details") {
val planet = navController
.previousBackStackEntry
?.arguments
?.getParcelable<Planet>("planet")
planet?.let {
DetailsScreen(it, navController)
}
}
}
}
The part where I try to make the navigation happen to the details page is in this function:
private fun navigateToPlanet(navController: NavHostController, planet: Planet) {
navController.currentBackStackEntry?.arguments = Bundle().apply {
putParcelable("planet", planet)
}
navController.navigate("details")
}
I've already tried simply applying to the recurring arguments
of the navigateToPlanet
function using apply
but it doesn't work, the screen opens blank without any information. This is the code for my failed attempt:
private fun navigateToPlanet(navController: NavHostController, planet: Planet) {
navController.currentBackStackEntry?.arguments?.apply {
putParcelable("planet", planet)
}
navController.navigate("details")
}
NOTE: The Jetpack Compose team doesn’t recommend passing Parcelable in the navigation composable routes.
Navigation compose also supports passing arguments between composable destinations. In order to do this, you need to add argument placeholders to your route, similar to how you add arguments to a deep link when using the base navigation library: NavHost (startDestination = "profile/ {userId}") { ... composable ("profile/ {userId}") {...} }
The way it is currently done in jetpack compose is by appending the route with arguments and their respective values. For example, let’s say that we have a composable with route = “userPage”, and we want to pass arguments “userId” and “isLoggedIn”. The following snippets show how to do that in jetpack compose.
The Navigation component provides support for Jetpack Compose applications. You can navigate between composables while taking advantage of the Navigation component’s infrastructure and features. Note: If you are not familiar with Compose, review the Jetpack Compose resources before continuing.
As per the Navigation documentation:
Caution: Passing complex data structures over arguments is considered an anti-pattern. Each destination should be responsible for loading UI data based on the minimum necessary information, such as item IDs. This simplifies process recreation and avoids potential data inconsistencies.
You shouldn't be passing Parcelables at all as arguments and never has been a recommended pattern: not in Navigation 2.4.0-alpha07 nor in Navigation 2.4.0-alpha08. Instead, you should be reading data from a single source of truth. In your case, this is your Planet.data
static array, but would normally be a repository layer, responsible for loading data for your app.
This means what you should be passing through to your DetailsScreen
is not a Planet
itself, but the unique key that defines how to retrieve that Planet
object. In your simple case, this might just be the index of the selected Planet.
By following the guide for navigating with arguments, this means your graph would look like:
@Composable
private fun NavigationComponent(navController: NavHostController) {
NavHost(navController = navController, startDestination = HOME) {
composable(HOME) { HomeScreen(navController) }
composable(
"$DETAILS/{index}",
arguments = listOf(navArgument("index") { type = NavType.IntType }
) { backStackEntry ->
val index = backStackEntry.arguments?.getInt("index") ?: 0
// Read from our single source of truth
// This means if that data later becomes *not* static, you'll
// be able to easily substitute this out for an observable
// data source
val planet = Planet.data[index]
DetailsScreen(planet, navController)
}
}
}
As per the Testing guide for Navigation Compose, you shouldn't be passing your NavController
down through your hierarchy - this code cannot be easily tested and you can't use @Preview
to preview your composables. Instead, you should:
- Pass only parsed arguments into your composable
- Pass lambdas that should be triggered by the composable to navigate, rather than the NavController itself.
So you shouldn't be passing your NavController
down to HomeScreen
or DetailsScreen
at all. You might start this effort to make your code more testable by first changing your usage of it in your PlanetCard
, which should take a lambda, instead of a NavController
:
@Composable
private fun PlanetCard(planet: Planet, onClick: () -> Unit) {
Card(
elevation = 4.dp,
shape = RoundedCornerShape(15.dp),
border = BorderStroke(
width = 2.dp,
color = Color(0x77f5f5f5),
),
modifier = Modifier
.fillMaxWidth()
.padding(5.dp)
.height(120.dp)
.clickable { onClick() }
) {
...
}
}
This means your PlanetList
can be written as:
@Composable
private fun PlanetList(navController: NavHostController) {
LazyColumn {
itemsIndexed(Planet.data) { index, planet ->
PlanetCard(planet) {
// Here we pass the index of the selected item as an argument
navController.navigate("${MainActivity.DETAILS}/$index")
}
}
}
}
You can see how continuing to use lambdas up the hierarchy would help encapsulate your MainActivity
constants in that class alone, instead of spreading them across your code base.
By switching to using an index, you've avoiding creating a second source of truth (your arguments themselves) and instead set yourself up to write testable code that will support further expansion beyond a static set of data.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With