Freelancing for Pale Blue

Looking for flexible work opportunities that fit your schedule?


Type-Safe Navigation in Jetpack Compose: Passing Custom Classes

Android Nov 20, 2024

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!

Tags

Great! You've successfully subscribed.
Great! Next, complete checkout for full access.
Welcome back! You've successfully signed in.
Success! Your account is fully activated, you now have access to all content.