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?
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.
EpicPandaForce