DEV Community

RockAndNull
RockAndNull

Posted on • Originally published at paleblueapps.com on

Type-Safe Navigation in Jetpack Compose: Passing Custom Classes

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
)
Enter fullscreen mode Exit fullscreen mode

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)
}

Enter fullscreen mode Exit fullscreen mode

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!

Image of Datadog

The Essential Toolkit for Front-end Developers

Take a user-centric approach to front-end monitoring that evolves alongside increasingly complex frameworks and single-page applications.

Get The Kit

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay