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
Change Device Language via Adb
Relocate Google Logo in Mapview
How to Return String or JSONobject from Asynchronous Callback Using Retrofit
In Espresso, How to Avoid Ambiguousviewmatcherexception When Multiple Views Match
5.1 Crash - a Taskdescription's Primary Color Should Be Opaque
Android Sdk Tools Revision 22 Issue
How to Delete Internal Storage File in Android
Only One Log in Per User at the Time in Firebase for Android
Onserviceconnected Never Called After Bindservice Method
Failed to Read Png Signature: File Does Not Start with Png Signature
Cannot Deserialize Instance of Object Out of Start_Array Token in Spring Webservice
Android Sharedpreferences , How to Save a Simple Int Variable
Uploading Image from Android to Gcs
Android Dialog, Keep Dialog Open When Button Is Pressed
Android:Dither="True" Does Not Dither, What's Wrong
Android Sdk Content Loader Failing with Nullpointerexception