Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How pass parcelable argument with new version of compose navigation?

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")
}
like image 987
Pierre Vieira Avatar asked Sep 04 '21 21:09

Pierre Vieira


People also ask

Can I Pass parcelable in the navigation composable routes?

NOTE: The Jetpack Compose team doesn’t recommend passing Parcelable in the navigation composable routes.

How do I pass arguments from one navigation route to another?

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}") {...} }

How do I pass arguments to a route in jetpack compose?

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.

What is the navigation component used for?

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.


1 Answers

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.

like image 196
ianhanniballake Avatar answered Nov 15 '22 07:11

ianhanniballake