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.