我想使用组合导航将 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)
以外的函数的对象传递?
答案 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.createGraph
、NavGraphBuilder.composable
和 androidx.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
不同,这不会崩溃,而是返回一个可为空的类型。