使用撰写导航传递 Parcelable 参数

时间:2021-01-07 09:48:46

标签: android kotlin android-jetpack-compose android-jetpack-navigation

我想使用组合导航将 Parcelable 对象 (BluetoothDevice) 传递给可组合对象。

传递原始类型很容易:

composable(
  "profile/{userId}",
  arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
navController.navigate("profile/user1234")

但是我不能在路由中传递一个parcelable对象,除非我可以将它序列化为一个字符串。

composable(
  "deviceDetails/{device}",
  arguments = listOf(navArgument("device") { type = NavType.ParcelableType(BluetoothDevice::class.java) })
) {...}
val device: BluetoothDevice = ...
navController.navigate("deviceDetails/$device")

上面的代码显然不起作用,因为它只是隐式调用了 toString()

有没有办法将 Parcelable 序列化为 String 以便我可以在路由中传递它,或者将导航参数作为具有除 navigate(route: String) 以外的函数的对象传递?

5 个答案:

答案 0 :(得分:15)

编辑:更新为beta-07

基本上你可以做到以下几点:

// In the source screen...
navController.currentBackStackEntry?.arguments = 
    Bundle().apply {
        putParcelable("bt_device", device)
    }
navController.navigate("deviceDetails")

在详细信息屏幕中...

val device = navController.previousBackStackEntry
    ?.arguments?.getParcelable<BluetoothDevice>("bt_device")

答案 1 :(得分:2)

如果我们在 popUpTo(...) 上弹出 (navigate(...)) 返回堆栈,@nglauber 给出的 backStackEntry 解决方案将不起作用。

所以这是另一种解决方案。我们可以通过将对象转换为 JSON 字符串来传递对象。

示例代码:

val ROUTE_USER_DETAILS = "user-details/{user}"


// Pass data (I am using Moshi here)
val user = User(id = 1, name = "John Doe") // User is a data class.

val moshi = Moshi.Builder().build()
val jsonAdapter = moshi.adapter(User::class.java).lenient()
val userJson = jsonAdapter.toJson(user)

navController.navigate(
    ROUTE_USER_DETAILS.replace("{user}", userJson)
)


// Receive Data
NavHost {
    composable(ROUTE_USER_DETAILS) { backStackEntry ->
        val userJson =  backStackEntry.arguments?.getString("user")
        val moshi = Moshi.Builder().build()
        val jsonAdapter = moshi.adapter(User::class.java).lenient()
        val userObject = jsonAdapter.fromJson(userJson)

        UserDetailsView(userObject) // Here UserDetailsView is a composable.
    }
}


// Composable function/view
@Composable
fun UserDetailsView(
    user: User
){
    // ...
}

答案 2 :(得分:1)

按照 nglauber 的建议,我创建了两个对我有帮助的扩展

@Suppress("UNCHECKED_CAST")
fun <T> NavHostController.getArgument(name: String): T {
    return previousBackStackEntry?.arguments?.getSerializable(name) as? T
        ?: throw IllegalArgumentException()
}

fun NavHostController.putArgument(name: String, arg: Serializable?) {
    currentBackStackEntry?.arguments?.putSerializable(name, arg)
}

我是这样使用它们的:

Source:
navController.putArgument(NavigationScreens.Pdp.Args.game, game)
navController.navigate(NavigationScreens.Pdp.route)

Destination:
val game = navController.getArgument<Game>(NavigationScreens.Pdp.Args.game)
PdpScreen(game)

答案 3 :(得分:0)

我有一个类似的问题,我必须传递一个包含斜杠的字符串,因为它们被用作深层链接参数的分隔符,我不能这样做。逃离他们对我来说似乎并不“干净”。

我想出了以下解决方法,可以根据您的情况轻松调整。我将 NavHost 中的 NavController.createGraphNavGraphBuilder.composableandroidx.navigation.compose 改写如下:

@Composable
fun NavHost(
    navController: NavHostController,
    startDestination: Screen,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) {
    NavHost(navController, remember(route, startDestination, builder) {
        navController.createGraph(startDestination, route, builder)
    })
}

fun NavController.createGraph(
    startDestination: Screen,
    route: String? = null,
    builder: NavGraphBuilder.() -> Unit
) = navigatorProvider.navigation(route?.hashCode() ?: 0, startDestination.hashCode(), builder)

fun NavGraphBuilder.composable(
    screen: Screen,
    content: @Composable (NavBackStackEntry) -> Unit
) {
    addDestination(ComposeNavigator.Destination(provider[ComposeNavigator::class], content).apply {
        id = screen.hashCode()
    })
}

其中 Screen 是我的目的地枚举

sealed class Screen {
    object Index : Screen()
    object Example : Screen()
}

请注意,我删除了深层链接和参数,因为我没有使用它们。这仍然允许我手动传递和检索参数,并且可以重新添加该功能,我的情况根本不需要它。

假设我想要 Example 接受一个字符串参数 path

const val ARG_PATH = "path"

然后我像这样初始化 NavHost

NavHost(navController, startDestination = Screen.Index) {
    composable(Screen.Index) { IndexScreen(::navToExample) }

    composable(Screen.Example) { navBackStackEntry ->
        navBackStackEntry.arguments?.getString(ARG_PATH)?.let { path ->
            ExampleScreen(path, ::navToIndex)
        }
    }
}

这就是我通过 Example 导航到 path 的方式

fun navToExample(path: String) {
    navController.navigate(Screen.Example.hashCode(), Bundle().apply {
        putString(ARG_PATH, path)
    })
}

我相信这可以改进,但这些是我最初的想法。 要启用深层链接,您需要恢复使用

// composable() and other places
val internalRoute = "android-app://androidx.navigation.compose/$route"
id = internalRoute.hashCode()

答案 4 :(得分:0)

这是另一种解决方案,它也可以通过将 Parcelable 添加到正确的 NavBackStackEntry而不是上一个条目而起作用。这个想法是首先调用 navController.navigate,然后将参数添加到 NavBackStackEntry.arguments 中的最后一个 NavController.backQueue。请注意,这确实使用了另一个库组受限 API(用 RestrictTo(LIBRARY_GROUP) 注释),因此可能会中断。其他一些人发布的解决方案使用受限制的 NavBackStackEntry.arguments,但 NavController.backQueue 也受限制。

以下是用于导航的 NavController 和用于检索可组合路由中的参数的 NavBackStackEntry 的一些扩展:


fun NavController.navigate(
    route: String,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null,
    args: List<Pair<String, Parcelable>>? = null,
) {
    if (args == null || args.isEmpty()) {
        navigate(route, navOptions, navigatorExtras)
        return
    }
    navigate(route, navOptions, navigatorExtras)
    val addedEntry: NavBackStackEntry = backQueue.last()
    val argumentBundle: Bundle = addedEntry.arguments ?: Bundle().also {
        addedEntry.arguments = it
    }
    args.forEach { (key, arg) ->
        argumentBundle.putParcelable(key, arg)
    }
}

inline fun <reified T : Parcelable> NavController.navigate(
    route: String,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null,
    arg: T? = null,
    
) {
    if (arg == null) {
        navigate(route, navOptions, navigatorExtras)
        return
    }
    navigate(
        route = route,
        navOptions = navOptions,
        navigatorExtras = navigatorExtras,
        args = listOf(T::class.qualifiedName!! to arg),
    )
}

fun NavBackStackEntry.requiredArguments(): Bundle = arguments ?: throw IllegalStateException("Arguments were expected, but none were provided!")

@Composable
inline fun <reified T : Parcelable> NavBackStackEntry.rememberRequiredArgument(
    key: String = T::class.qualifiedName!!,
): T = remember {
    requiredArguments().getParcelable<T>(key) ?: throw IllegalStateException("Expected argument with key: $key of type: ${T::class.qualifiedName!!}")
}

@Composable
inline fun <reified T : Parcelable> NavBackStackEntry.rememberArgument(
    key: String = T::class.qualifiedName!!,
): T? = remember {
    arguments?.getParcelable(key)
}

要使用单个参数进行导航,您现在可以在 NavGraphBuilder 的范围内执行此操作:

composable(route = "screen_1") {
    Button(
        onClick = {
            navController.navigate(
                route = "screen_2",
                arg = MyParcelableArgument(whatever = "whatever"),
            )
        }
    ) {
        Text("goto screen 2")
    }
}
composable(route = "screen_2") { entry ->
    val arg: MyParcelableArgument = entry.rememberRequiredArgument()
    // TODO: do something with arg
}

或者如果你想传递多个相同类型的参数:

composable(route = "screen_1") {
    Button(
        onClick = {
            navController.navigate(
                route = "screen_2",
                args = listOf(
                    "arg_1" to MyParcelableArgument(whatever = "whatever"),
                    "arg_2" to MyParcelableArgument(whatever = "whatever"),
                ),
            )
        }
    ) {
        Text("goto screen 2")
    }
}
composable(route = "screen_2") { entry ->
    val arg1: MyParcelableArgument = entry.rememberRequiredArgument(key = "arg_1")
    val arg2: MyParcelableArgument = entry.rememberRequiredArgument(key = "arg_2")
    // TODO: do something with args
}

这种方法的主要好处是类似于使用 Moshi 序列化参数的答案,当在 popUpTo 中使用 navOptions 时它会起作用,但也会更有效,因为没有涉及JSON序列化。

据我所知,这不会在过程娱乐中幸存下来,当然也不适用于深层链接。对于需要保留娱乐或支持深层链接的情况,您可以使用 entry.rememberArgument 扩展将这些参数视为可选参数。与 entry.rememberRequiredArgument 不同,这不会崩溃,而是返回一个可为空的类型。