Search code examples
androidkotlinandroid-jetpack-composeandroid-jetpack-navigation

Pass Parcelable argument with compose navigation


I want to pass a parcelable object (BluetoothDevice) to a composable using compose navigation.

Passing primitive types is easy:

composable(
  "profile/{userId}",
  arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
navController.navigate("profile/user1234")

But I can't pass a parcelable object in the route unless I can serialize it to a string.

composable(
  "deviceDetails/{device}",
  arguments = listOf(navArgument("device") { type = NavType.ParcelableType(BluetoothDevice::class.java) })
) {...}
val device: BluetoothDevice = ...
navController.navigate("deviceDetails/$device")

The code above obviously doesn't work because it just implicitly calls toString().

Is there a way to either serialize a Parcelable to a String so I can pass it in the route or pass the navigation argument as an object with a function other than navigate(route: String)?


Solution

  • 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")