androidandroid-jetpack-composeandroid-jetpack-navigationkotlin-dsl

Jetpack compose navigation passing custom object with Kotlin DSL


I am trying type safety in Kotlin DSL and Navigation Compose but it looks like it is not working as expected, following is my code:

Screens:

@Serializable
data object SelectMenuNav

@Serializable
data class SelectProductNav(
    val menuSelection: MenuSelection
)

@Serializable
data class ProductDetailsNav(
    val menuSelection: MenuSelection,
    val product: Product
)

Custom Object I want to pass through composable

object CustomNavType {
    val ProductType = object : NavType<Product>(
        isNullableAllowed = false
    ) {
        override fun get(bundle: Bundle, key: String): Product? {
            return Json.decodeFromString(bundle.getString(key) ?: return null)
        }

        override fun parseValue(value: String): Product {
            return Json.decodeFromString(Uri.decode(value))
        }

        override fun put(bundle: Bundle, key: String, value: Product) {
            bundle.putString(key, Json.encodeToString(value))
        }

        override fun serializeAsValue(value: Product): String {
            return Uri.encode(Json.encodeToString(value))
        }
    }
}

My app navigation

@Composable
fun AppNavigation(modifier: Modifier = Modifier) {
    val navController = rememberNavController()
    NavHost(
        navController = navController,
        startDestination = SelectMenuNav
    ) {
        composable<SelectMenuNav> {
            ChooseMenuScreen(onSelection = { menuSelection ->
                navController.navigate(
                    SelectProductNav(
                        menuSelection = menuSelection
                    )
                )
            })
        }
        composable<SelectProductNav>(
            typeMap = mapOf(
                typeOf<MenuSelection>() to NavType.EnumType(MenuSelection::class.java)
            )
        ) {
            val menuSelection = it.toRoute<SelectProductNav>().menuSelection
            ChooseProductScreen(
                menuSelection = menuSelection,
                viewModel = hiltViewModel(),
                onNavigateToProductDetails = { product ->
                    navController.navigate(
                      ProductDetailsNav(
                          product = product,
                          menuSelection = menuSelection
                      )
                    )
                })
        }
        composable<ProductDetailsNav>(
            typeMap = mapOf(
                typeOf<Product>() to CustomNavType.ProductType,
                typeOf<MenuSelection>() to NavType.EnumType(MenuSelection::class.java)
            )
        ) { backStackEntry ->
            Log.d("AppNavigation", "ProductDetailsNav: ${backStackEntry.toRoute<ProductDetailsNav>()}")
            ProductDetailsScreen(
                modifier = Modifier,
                vm = hiltViewModel(),
                navigateToEditor = {},
                navigateBack = {}
            )
        }
    }
}

Even though Log is fine showing the product that has passed successfully between composables:

D  ProductDetailsNav: ProductDetailsNav(menuSelection=NAVA, product=Product(id=4371, productName=[...]

My viewmodel throws an error:

  java.lang.IllegalArgumentException: Route app.xml.aionianew.navigation.ProductDetailsNav could not find any NavType for argument product of type app.xml.aionianew.database.internal.data.Product - typeMap received was {}
                                                                                                        at androidx.navigation.serialization.RouteSerializerKt$generateNavArguments$2$1.invoke(RouteSerializer.kt:108)
                                                                                                        at androidx.navigation.serialization.RouteSerializerKt$generateNavArguments$2$1.invoke(RouteSerializer.kt:103)
                                                                                                        at androidx.navigation.NamedNavArgumentKt.navArgument(NamedNavArgument.kt:21)
                                                                                                        at androidx.navigation.serialization.RouteSerializerKt.generateNavArguments(RouteSerializer.kt:103)
                                                                                                        at androidx.navigation.SavedStateHandleKt.internalToRoute(SavedStateHandle.kt:50)
                                                                                                        at app.xml.aionianew.screens.product_details.ViewModelProductDetails.<init>(ViewModelProductDetails.kt:1018)
                                                                                                        at app.xml.aionianew.DaggerMyApp_HiltComponents_SingletonC$ViewModelCImpl$SwitchingProvider.get(DaggerMyApp_HiltComponents_SingletonC.java:497)
                                                                                                        at dagger.hilt.android.internal.lifecycle.HiltViewModelFactory$2.createViewModel(HiltViewModelFactory.java:133)

My code in viewmodel is like so:

@HiltViewModel
class ViewModelProductDetails @Inject constructor(
    private val uc: UseCasesProductDetails,
    savedState: SavedStateHandle
) : ViewModel() {

    private val selectedProduct: Product = savedState.toRoute<ProductDetailsNav>().product
    private val menuSelection:MenuSelection = savedState.toRoute<ProductDetailsNav>().menuSelection

So the question is what am I missing...


Solution

  • savedState.toRoute also takes a typeMap - without that typeMap, it doesn't know how to convert the SavedStateHandle back into your object so you'll want to pass in the same typeMap as you used in your navigation graph definition in your ViewModel:

    private val productDetails = savedState.toRoute<ProductDetailsNav>(
        typeMap = mapOf(
            typeOf<Product>() to CustomNavType.ProductType,
            typeOf<MenuSelection>() to NavType.EnumType(MenuSelection::class.java)
        )
    )
    private val selectedProduct: Product = productDetails.product
    private val menuSelection: MenuSelection = productDetails.menuSelection