Pass Parcelable Argument with Compose Navigation

Pass Parcelable argument with compose navigation

Warning:
Ian Lake is an Android Developer Advocate and he says in this answer that pass complex data structures is an anti-pattern (referring the documentation). He works on this library, so he has authority on this. Use the approach below by your own.

Edit: Updated to Compose Navigation 2.4.0-beta07

Seems like previous solution is not supported anymore. Now you need to create a custom NavType.

Let's say you have a class like:

@Parcelize
data class Device(val id: String, val name: String) : Parcelable

Then you need to define a NavType

class AssetParamType : NavType<Device>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): Device? {
return bundle.getParcelable(key)
}

override fun parseValue(value: String): Device {
return Gson().fromJson(value, Device::class.java)
}

override fun put(bundle: Bundle, key: String, value: Device) {
bundle.putParcelable(key, value)
}
}

Notice that I'm using Gson to convert the object to a JSON string. But you can use the conversor that you prefer...

Then declare your composable like this:

NavHost(...) {
composable("home") {
Home(
onClick = {
val device = Device("1", "My device")
val json = Uri.encode(Gson().toJson(device))
navController.navigate("details/$json")
}
)
}
composable(
"details/{device}",
arguments = listOf(
navArgument("device") {
type = AssetParamType()
}
)
) {
val device = it.arguments?.getParcelable<Device>("device")
Details(device)
}
}

Original answer

Basically you can do the following:

// In the source screen...
navController.currentBackStackEntry?.arguments =
Bundle().apply {
putParcelable("bt_device", device)
}
navController.navigate("deviceDetails")

And in the details screen...

val device = navController.previousBackStackEntry
?.arguments?.getParcelable<BluetoothDevice>("bt_device")

How pass parcelable argument with new version of compose navigation?

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.

How to pass object in navigation in jetpack compose?

The following workarounds based on navigation-compose version 2.4.0-alpha05.


I found 2 workarounds for passing objects.

1. Convert the object into JSON string:

Here we can pass the objects using the JSON string representation of the object.

Example code:

val ROUTE_USER_DETAILS = "user-details/user={user}"

// Pass data (I am using Moshi here)
val user = User(id = 1, name = "John Doe") // User is a data class.

val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userJson = jsonAdapter.toJson(user)

navController.navigate(
ROUTE_USER_DETAILS.replace("{user}", userJson)
)

// Receive Data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userJson = backStackEntry.arguments?.getString("user")
val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userObject = jsonAdapter.fromJson(userJson)

UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}

// Composable function/view
@Composable
fun UserDetailsView(
user: User
){
// ...
}

2. Passing the object using NavBackStackEntry:

Here we can pass data using navController.currentBackStackEntry and receive data using navController.previousBackStackEntry.

Example code:

val ROUTE_USER_DETAILS = "user-details/{user}"

// Pass data
val user = User(id = 1, name = "John Doe") // User is a parcelable data class.

navController.currentBackStackEntry?.arguments?.putParcelable("user", user)
navController.navigate(ROUTE_USER_DETAILS)

// Receive data
NavHost {
composable(ROUTE_USER_DETAILS) { backStackEntry ->
val userObject = navController.previousBackStackEntry?.arguments?.getParcelable<User>("user")

UserDetailsView(userObject) // Here UserDetailsView is a composable.
}
}

// Composable function/view
@Composable
fun UserDetailsView(
user: User
){
// ...
}

Important Note: The 2nd solution will not work if we pop up back stacks on navigate.

How to pass a list of serializable objects in Jetpack Navigation?

So I made it by wrapping up my list of objects into another serializable data class that looks like that:

data class Posts(
val posts: List<Post>
) : Serializable

And navigated to another fragment this way:

val action = PostsFragmentDirections.actionOpenPostsCluster(Posts(posts))
navController.navigate(action)

Navigation documentation says that passing complex data structures over arguments is considered an anti-pattern(you can find it in the comment above by Phil Dukhov), therefore I think I can't recommend it



Related Topics



Leave a reply



Submit