Type-Safe Navigation in Jetpack Compose: Passing Custom Classes
Jetpack Compose's Navigation library has introduced long-awaited type safety, making navigation between destinations more robust, intuitive, and of-course safe.
But what happens when you need to pass custom classes as arguments? Luckily, the library supports this functionality - with some additional setup. Here's a complete concise example to guide you.
Consider a CheckoutFlow sealed interface with a Calendar destination accepting an Offer
object as an argument:
fun NavGraphBuilder.checkoutGraph(navController: NavHostController) {
navigation<CheckoutFlow.Start>(
startDestination = CheckoutFlow.Calendar(offer = null),
) {
composable<CheckoutFlow.Calendar>(
typeMap = mapOf(
typeMapOf<Offer?>(),
),
) {
val route: CheckoutFlow.Calendar = it.toRoute()
CheckoutCalendarComponent(
offer = route.offer,
)
}
}
}
sealed interface CheckoutFlow {
@Serializable
data object Start : CheckoutFlow
@Serializable
data class Calendar(
val offer: Offer?,
) : CheckoutFlow
}
@Serializable
data class Offer(
@SerialName("description")
val description: String,
@SerialName("id")
val id: Int
)
The Offer
class, marked with @Serializable
, requires additional work for navigation argument handling. Use the following custom NavType
implementation for serialization and deserialization:
inline fun <reified T> serializableNavType(isNullableAllowed: Boolean = false) =
object : NavType<T>(isNullableAllowed = isNullableAllowed) {
override fun put(bundle: Bundle, key: String, value: T) {
bundle.putString(key, serializeAsValue(value))
}
override fun get(bundle: Bundle, key: String): T? {
return bundle.getString(key)?.let { parseValue(it) }
}
override fun serializeAsValue(value: T): String {
return Uri.encode(Json.encodeToString(value))
}
override fun parseValue(value: String): T {
return Json.decodeFromString(Uri.decode(value))
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other !is NavType<*>) return false
if (other::class.java != this::class.java) return false
if (isNullableAllowed != other.isNullableAllowed) return false
return true
}
}
inline fun <reified T> typeMapOf(): Pair<KType, NavType<T>> {
val type = typeOf<T>()
return type to serializableNavType<T>(isNullableAllowed = type.isMarkedNullable)
}
Serialization and deserialization are handled by the serializableNavType
, which converts your class to and from a string representation.
Additionally, extremely important is the equals
method that plays a crucial role in ensuring arguments are compared accurately during navigation.
By implementing these steps, you can seamlessly pass custom classes in a type-safe manner, unlocking the full potential of Jetpack Compose's Navigation library.
Happy coding!