How to pass a custom Parcelable object in Jetpack Compose Type-Safe Navigation (Navigation 2.8.0+)?


With the release of Jetpack Compose Navigation 2.8.0, we can now navigate using type-safe objects (@Serializable data classes) instead of route strings. However, if my route data class contains a custom Parcelable object, the navigation component doesn't know how to handle it by default. For example, I have a sealed class for my routes:

@Serializable
    sealed class Route {
        @Serializable
        data class DetailsScreen(val userData: UserData) : Route()
    }

Where UserData is a @Parcelize class. How do I define and register a custom NavType for this Parcelable object so I can pass it safely in the new Compose navigation graph?

2
May 11 at 10:07 PM
User AvatarMichael B
#android#kotlin#android-jetpack-compose#android-navigation

Accepted Answer

You need to declare a custom NavType that declares how to put the class into a Bundle, and also how to serialize/deserialize it to a string. You have to do that for each custom type you have, and the best part is that you have to pass the typeMap to whenever you want to deserialize the class from an entry.

Example code:

val CustomClassNavType = object : NavType<CustomClass>(
    isNullableAllowed = false
) {
    @Suppress("DEPRECATION") // you can use BundleCompat.getParcelable() if you want
    override fun get(bundle: Bundle, key: String): CustomClass? =
        bundle.getParcelable(key) // assuming CustomClass is Parcelable

    override fun parseValue(value: String): CustomClass {
        val base64 = Base64.decode(value, Base64.DEFAULT) // use Base64 so that it can be safely appended to a string, basically escaping
        val base64DecodedString = String(base64, Charset.forName("UTF-8")) // make string from byte-array
        val jsonResult = Json.decodeFromString<CustomClass>(base64DecodedString) // use kotlinx.serialization
        return jsonResult
    }

    override fun serializeAsValue(value: CustomClass): String =
        Base64.encodeToString( // use Base64 so that it can be safely appended to a string, basically escaping
            Json.encodeToString(value = value) // use kotlinx.serialization
               .toByteArray(Charset.forName("UTF-8")), // base64 needs a byte-array
            Base64.DEFAULT,
        ) 

    override fun put(bundle: Bundle, key: String, value: CustomClass) {
        bundle.putParcelable(key, value) // assume class is parcelable
    }
}

@Parcelize
@Serializable
data class CustomClass(
    // ... everything must be parcelable + serializable or a primitive
): Parcelable, java.io.Serializable

@Parcelable
@Serializable
data class ExampleScreenRoute(
    val customClass: CustomClass,
): Parcelable, java.io.Serializable

@Composable
fun NavHost() {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = StartScreenRoute,
    ) {
        composable<ExampleScreenRoute>(
            typeMap = mapOf(typeOf<CustomClass>() to CustomClassNavType),
        ) { entry ->
            val exampleScreenRoute = entry.toRoute<ExampleScreenRoute>()

            ExampleScreenComposable(navController, exampleScreenRoute.customClass)
        }
    }
}

The best part is that if you forget the type-map, the app will crash at runtime. You have to pass in every type that is contained within your route in order to be able to de-serialize it.

User AvatarEpicPandaForce
May 12 at 10:38 AM
0