You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose导航Kotlin DSL传递自定义对象类型安全异常排查

问题

我正在用Jetpack Compose Navigation的Kotlin DSL实现类型安全导航,但遇到了问题。相关代码如下:

页面路由定义

@Serializable
data object SelectMenuNav

@Serializable
data class SelectProductNav(
    val menuSelection: MenuSelection
)

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

自定义导航类型(传递Product对象)

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

应用导航实现

@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 = {}
            )
        }
    }
}

日志显示Product对象已成功传递:

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

但ViewModel抛出错误:

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)

ViewModel代码:

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

请问我遗漏了什么配置?


解决方案

问题出在ViewModel中调用savedState.toRoute<ProductDetailsNav>()时,没有传入对应的typeMap参数。你在NavHost的composable块里配置的typeMap只作用于Composable函数内的backStackEntry.toRoute()调用,ViewModel里的SavedStateHandle并不知道这个配置。

有两种解决方式:

方式一:调用toRoute时传入typeMap

修改ViewModel代码,在调用toRoute时手动传入对应的类型映射:

@HiltViewModel
class ViewModelProductDetails @Inject constructor(
    private val uc: UseCasesProductDetails,
    savedState: SavedStateHandle
) : ViewModel() {
    private val typeMap = mapOf(
        typeOf<Product>() to CustomNavType.ProductType,
        typeOf<MenuSelection>() to NavType.EnumType(MenuSelection::class.java)
    )

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

方式二:全局注册自定义NavType

如果不想在每个ViewModel里重复传入typeMap,可以全局注册自定义类型,这样toRoute会自动使用全局配置:

  1. 定义全局类型映射:
object GlobalNavTypeMap {
    val globalTypeMap = mapOf(
        typeOf<Product>() to CustomNavType.ProductType,
        typeOf<MenuSelection>() to NavType.EnumType(MenuSelection::class.java)
    )
}
  1. 在Application子类的onCreate方法中注册全局配置:
class MyApp : Application() {
    override fun onCreate() {
        super.onCreate()
        RouteSerializer.registerTypeMap(GlobalNavTypeMap.globalTypeMap)
    }
}

之后ViewModel里的savedState.toRoute<ProductDetailsNav>()就会自动使用全局配置,无需手动传入typeMap。

注意:全局注册的方式需要确保所有自定义类型都提前注册,避免遗漏导致类似错误。


内容的提问来源于stack exchange,提问作者F.Mysir

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 10:29:59