通过组合导航传递 Parcelable 参数
Pass Parcelable argument with compose navigation
我想使用组合导航将可打包对象 (BluetoothDevice
) 传递给可组合对象。
传递基本类型很容易:
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
navController.navigate("profile/user1234")
但是我无法在路由中传递可打包对象,除非我可以将其序列化为字符串。
composable(
"deviceDetails/{device}",
arguments = listOf(navArgument("device") { type = NavType.ParcelableType(BluetoothDevice::class.java) })
) {...}
val device: BluetoothDevice = ...
navController.navigate("deviceDetails/$device")
上面的代码显然不起作用,因为它只是隐式调用了 toString()
.
有没有办法将 Parcelable
序列化为 String
以便我可以在路由中传递它或将导航参数作为具有 [=18= 以外的函数的对象传递]?
警告:
Ian Lake is an Android Developer Advocate and he says in 传递复杂数据结构是一种反模式(参考文档)。他在这个图书馆工作,所以他有这方面的权力。自己使用下面的方法。
编辑: 已更新为撰写导航 2.4.0-beta07
似乎不再支持以前的解决方案。现在您需要创建自定义 NavType
.
假设您 class 喜欢:
@Parcelize
data class Device(val id: String, val name: String) : Parcelable
那么你需要定义一个NavType
class AssetParamType : NavType<Device>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): Device? {
return bundle.getParcelable(key)
}
override fun parseValue(value: String): Device {
return Gson().fromJson(value, Device::class.java)
}
override fun put(bundle: Bundle, key: String, value: Device) {
bundle.putParcelable(key, value)
}
}
请注意,我正在使用 Gson
将对象转换为 JSON 字符串。但是你可以使用你喜欢的转换器...
然后像这样声明您的可组合项:
NavHost(...) {
composable("home") {
Home(
onClick = {
val device = Device("1", "My device")
val json = Uri.encode(Gson().toJson(device))
navController.navigate("details/$json")
}
)
}
composable(
"details/{device}",
arguments = listOf(
navArgument("device") {
type = AssetParamType()
}
)
) {
val device = it.arguments?.getParcelable<Device>("device")
Details(device)
}
}
原回答
基本上你可以做到以下几点:
// 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")
我有一个类似的问题,我必须传递一个包含斜杠的字符串,并且由于它们被用作深度 link 参数的分隔符,我无法做到这一点。逃离他们对我来说似乎并不“干净”。
我想出了以下解决方法,可以根据您的情况轻松调整。我从 androidx.navigation.compose
重写了 NavHost
、NavController.createGraph
和 NavGraphBuilder.composable
如下:
@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()
}
请注意,我删除了深度 links 和参数,因为我没有使用它们。这仍然允许我手动传递和检索参数,并且可以重新添加该功能,我根本不需要它。
假设我希望 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)
}
}
}
这就是我通过 path
导航到 Example
的方式
fun navToExample(path: String) {
navController.navigate(Screen.Example.hashCode(), Bundle().apply {
putString(ARG_PATH, path)
})
}
我确信这可以改进,但这些是我最初的想法。
要启用深度 links,您需要恢复使用
// composable() and other places
val internalRoute = "android-app://androidx.navigation.compose/$route"
id = internalRoute.hashCode()
根据 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)
如果我们在 navigate(...)
.
上弹出 (popUpTo(...)
) 后退堆栈,@nglauber 给出的 backStackEntry 解决方案将不起作用
所以这是另一个解决方案。我们可以通过将对象转换为 JSON 字符串来传递对象。
示例代码:
val ROUTE_USER_DETAILS = "user-details?user={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
){
// ...
}
这是另一种解决方案,它也可以通过将 Parcelable 添加到正确的 NavBackStackEntry
, 而不是先前的条目 。思路是先调用navController.navigate
,然后把参数加到NavController.backQueue
中的最后一个NavBackStackEntry.arguments
。请注意,这确实使用了另一个库组限制 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 序列化参数的答案,当在 navOptions
中使用 popUpTo
时它会起作用,但也会更有效率因为不涉及 JSON 序列化。
这当然不适用于深度链接,但它会在进程或 activity 重新创建时继续存在。对于需要支持深度链接或什至只是导航路由的可选参数的情况,您可以使用 entry.rememberArgument
扩展。与 entry.rememberRequiredArgument
不同,它将 return null 而不是抛出 IllegalStateException
.
我为 NavController 写了一个小扩展。
import android.os.Bundle
import androidx.core.net.toUri
import androidx.navigation.*
fun NavController.navigate(
route: String,
args: Bundle,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
val routeLink = NavDeepLinkRequest
.Builder
.fromUri(NavDestination.createRoute(route).toUri())
.build()
val deepLinkMatch = graph.matchDeepLink(routeLink)
if (deepLinkMatch != null) {
val destination = deepLinkMatch.destination
val id = destination.id
navigate(id, args, navOptions, navigatorExtras)
} else {
navigate(route, navOptions, navigatorExtras)
}
}
正如您所看到的,至少有 16 个函数“navigate”具有不同的参数,所以它只是一个转换器供使用
public open fun navigate(@IdRes resId: Int, args: Bundle?)
因此,使用此扩展,您可以使用 Compose Navigation,而无需这些可怕的深层 link 参数作为路由参数。
因为 nglauber 的 答案在前进时有效,而在向后导航时无效,你得到一个空值。我想也许至少暂时我们可以在我们的可组合项中使用 remember 保存传递的参数,并希望他们将 Parcelable 参数类型添加到路由导航中。
目标可组合目标:
composable("yourRout") { backStackEntry ->
backStackEntry.arguments?.let {
val rememberedProject = remember { mutableStateOf<Project?>(null) }
val project =
navController.previousBackStackEntry?.arguments?.getParcelable(
PROJECT_ARGUMENT_KEY
) ?: rememberedProject.value
rememberedProject.value = project
TargetScreen(
project = project ?: throw IllegalArgumentException("parcelable was null"),
)
}
这是源代码:触发导航:
navController.currentBackStackEntry?.arguments =
Bundle().apply {
putParcelable(PROJECT_ARGUMENT_KEY, project)
}
navController.navigate("yourRout")
这是我使用 BackStackEntry
的版本
用法:
composable("your_route") { entry ->
AwesomeScreen(entry.requiredArg("your_arg_key"))
}
navController.navigate("your_route", "your_arg_key" to yourArg)
扩展:
fun NavController.navigate(route: String, vararg args: Pair<String, Parcelable>) {
navigate(route)
requireNotNull(currentBackStackEntry?.arguments).apply {
args.forEach { (key: String, arg: Parcelable) ->
putParcelable(key, arg)
}
}
}
inline fun <reified T : Parcelable> NavBackStackEntry.requiredArg(key: String): T {
return requireNotNull(arguments) { "arguments bundle is null" }.run {
requireNotNull(getParcelable(key)) { "argument for $key is null" }
}
}
一个非常简单和基本的方法如下
1.First 创建要传递的可打包对象,例如
@Parcelize
data class User(
val name: String,
val phoneNumber:String
) : Parcelable
2.Then 在您所在的当前可组合项中,例如主屏幕
val userDetails = UserDetails(
name = "emma",
phoneNumber = "1234"
)
)
navController.currentBackStackEntry?.arguments?.apply {
putParcelable("userDetails",userDetails)
}
navController.navigate(Destination.DetailsScreen.route)
3.Then 在 details composable 中,确保将 navcontroller 作为参数传递给它,例如
@Composable
fun Details (navController:NavController){
val data = remember {
mutableStateOf(navController.previousBackStackEntry?.arguments?.getParcelable<UserDetails>("userDetails")!!)
}
}
N.B: 如果 parcelable 没有被传递到状态,你会在返回时收到一个错误
我对 Moshi
的处理方式:
路线
sealed class Route(
private val route: String,
val Key: String = "",
) {
object Main : Route(route = "main")
object Profile : Route(route = "profile", Key = "user")
override fun toString(): String {
return when {
Key.isNotEmpty() -> "$route/{$Key}"
else -> route
}
}
}
扩展
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.core.net.toUri
import androidx.navigation.*
import com.squareup.moshi.Moshi
inline fun <reified T> NavController.navigate(
route: String,
data: Pair<String, T>,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
) {
val count = route
.split("{${data.first}}")
.size
.dec()
if (count != 1) {
throw IllegalArgumentException()
}
val out = Moshi.Builder()
.build()
.adapter(T::class.java)
.toJson(data.second)
val newRoute = route.replace(
oldValue = "{${data.first}}",
newValue = Uri.encode(out),
)
navigate(
request = NavDeepLinkRequest.Builder
.fromUri(NavDestination.createRoute(route = newRoute).toUri())
.build(),
navOptions = navOptions,
navigatorExtras = navigatorExtras,
)
}
inline fun <reified T> NavBackStackEntry.getData(key: String): T? {
val data = arguments?.getString(key)
return when {
data != null -> Moshi.Builder()
.build()
.adapter(T::class.java)
.fromJson(data)
else -> null
}
}
@Composable
inline fun <reified T> NavBackStackEntry.rememberGetData(key: String): T? {
return remember { getData<T>(key) }
}
用法示例
data class User(
val id: Int,
val name: String,
)
@Composable
fun RootNavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "${Route.Main}",
) {
composable(
route = "${Route.Main}",
) {
Button(
onClick = {
navController.navigate(
route = "${Route.Profile}",
data = Route.Profile.Key to User(id = 1000, name = "John Doe"),
)
},
content = { Text(text = "Go to Profile") },
}
composable(
route = "${Route.Profile}",
arguments = listOf(
navArgument(name = Route.Profile.Key) { type = NavType.StringType },
),
) { entry ->
val user = entry.rememberGetData<User>(key = Route.Profile.Key)
Text(text = "$user")
}
}
}
我想使用组合导航将可打包对象 (BluetoothDevice
) 传递给可组合对象。
传递基本类型很容易:
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
navController.navigate("profile/user1234")
但是我无法在路由中传递可打包对象,除非我可以将其序列化为字符串。
composable(
"deviceDetails/{device}",
arguments = listOf(navArgument("device") { type = NavType.ParcelableType(BluetoothDevice::class.java) })
) {...}
val device: BluetoothDevice = ...
navController.navigate("deviceDetails/$device")
上面的代码显然不起作用,因为它只是隐式调用了 toString()
.
有没有办法将 Parcelable
序列化为 String
以便我可以在路由中传递它或将导航参数作为具有 [=18= 以外的函数的对象传递]?
警告:
Ian Lake is an Android Developer Advocate and he says in
编辑: 已更新为撰写导航 2.4.0-beta07
似乎不再支持以前的解决方案。现在您需要创建自定义 NavType
.
假设您 class 喜欢:
@Parcelize
data class Device(val id: String, val name: String) : Parcelable
那么你需要定义一个NavType
class AssetParamType : NavType<Device>(isNullableAllowed = false) {
override fun get(bundle: Bundle, key: String): Device? {
return bundle.getParcelable(key)
}
override fun parseValue(value: String): Device {
return Gson().fromJson(value, Device::class.java)
}
override fun put(bundle: Bundle, key: String, value: Device) {
bundle.putParcelable(key, value)
}
}
请注意,我正在使用 Gson
将对象转换为 JSON 字符串。但是你可以使用你喜欢的转换器...
然后像这样声明您的可组合项:
NavHost(...) {
composable("home") {
Home(
onClick = {
val device = Device("1", "My device")
val json = Uri.encode(Gson().toJson(device))
navController.navigate("details/$json")
}
)
}
composable(
"details/{device}",
arguments = listOf(
navArgument("device") {
type = AssetParamType()
}
)
) {
val device = it.arguments?.getParcelable<Device>("device")
Details(device)
}
}
原回答
基本上你可以做到以下几点:
// 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")
我有一个类似的问题,我必须传递一个包含斜杠的字符串,并且由于它们被用作深度 link 参数的分隔符,我无法做到这一点。逃离他们对我来说似乎并不“干净”。
我想出了以下解决方法,可以根据您的情况轻松调整。我从 androidx.navigation.compose
重写了 NavHost
、NavController.createGraph
和 NavGraphBuilder.composable
如下:
@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()
}
请注意,我删除了深度 links 和参数,因为我没有使用它们。这仍然允许我手动传递和检索参数,并且可以重新添加该功能,我根本不需要它。
假设我希望 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)
}
}
}
这就是我通过 path
Example
的方式
fun navToExample(path: String) {
navController.navigate(Screen.Example.hashCode(), Bundle().apply {
putString(ARG_PATH, path)
})
}
我确信这可以改进,但这些是我最初的想法。 要启用深度 links,您需要恢复使用
// composable() and other places
val internalRoute = "android-app://androidx.navigation.compose/$route"
id = internalRoute.hashCode()
根据 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)
如果我们在 navigate(...)
.
popUpTo(...)
) 后退堆栈,@nglauber 给出的 backStackEntry 解决方案将不起作用
所以这是另一个解决方案。我们可以通过将对象转换为 JSON 字符串来传递对象。
示例代码:
val ROUTE_USER_DETAILS = "user-details?user={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
){
// ...
}
这是另一种解决方案,它也可以通过将 Parcelable 添加到正确的 NavBackStackEntry
, 而不是先前的条目 。思路是先调用navController.navigate
,然后把参数加到NavController.backQueue
中的最后一个NavBackStackEntry.arguments
。请注意,这确实使用了另一个库组限制 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 序列化参数的答案,当在 navOptions
中使用 popUpTo
时它会起作用,但也会更有效率因为不涉及 JSON 序列化。
这当然不适用于深度链接,但它会在进程或 activity 重新创建时继续存在。对于需要支持深度链接或什至只是导航路由的可选参数的情况,您可以使用 entry.rememberArgument
扩展。与 entry.rememberRequiredArgument
不同,它将 return null 而不是抛出 IllegalStateException
.
我为 NavController 写了一个小扩展。
import android.os.Bundle
import androidx.core.net.toUri
import androidx.navigation.*
fun NavController.navigate(
route: String,
args: Bundle,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null
) {
val routeLink = NavDeepLinkRequest
.Builder
.fromUri(NavDestination.createRoute(route).toUri())
.build()
val deepLinkMatch = graph.matchDeepLink(routeLink)
if (deepLinkMatch != null) {
val destination = deepLinkMatch.destination
val id = destination.id
navigate(id, args, navOptions, navigatorExtras)
} else {
navigate(route, navOptions, navigatorExtras)
}
}
正如您所看到的,至少有 16 个函数“navigate”具有不同的参数,所以它只是一个转换器供使用
public open fun navigate(@IdRes resId: Int, args: Bundle?)
因此,使用此扩展,您可以使用 Compose Navigation,而无需这些可怕的深层 link 参数作为路由参数。
因为 nglauber 的 答案在前进时有效,而在向后导航时无效,你得到一个空值。我想也许至少暂时我们可以在我们的可组合项中使用 remember 保存传递的参数,并希望他们将 Parcelable 参数类型添加到路由导航中。
目标可组合目标:
composable("yourRout") { backStackEntry ->
backStackEntry.arguments?.let {
val rememberedProject = remember { mutableStateOf<Project?>(null) }
val project =
navController.previousBackStackEntry?.arguments?.getParcelable(
PROJECT_ARGUMENT_KEY
) ?: rememberedProject.value
rememberedProject.value = project
TargetScreen(
project = project ?: throw IllegalArgumentException("parcelable was null"),
)
}
这是源代码:触发导航:
navController.currentBackStackEntry?.arguments =
Bundle().apply {
putParcelable(PROJECT_ARGUMENT_KEY, project)
}
navController.navigate("yourRout")
这是我使用 BackStackEntry
用法:
composable("your_route") { entry ->
AwesomeScreen(entry.requiredArg("your_arg_key"))
}
navController.navigate("your_route", "your_arg_key" to yourArg)
扩展:
fun NavController.navigate(route: String, vararg args: Pair<String, Parcelable>) {
navigate(route)
requireNotNull(currentBackStackEntry?.arguments).apply {
args.forEach { (key: String, arg: Parcelable) ->
putParcelable(key, arg)
}
}
}
inline fun <reified T : Parcelable> NavBackStackEntry.requiredArg(key: String): T {
return requireNotNull(arguments) { "arguments bundle is null" }.run {
requireNotNull(getParcelable(key)) { "argument for $key is null" }
}
}
一个非常简单和基本的方法如下
1.First 创建要传递的可打包对象,例如
@Parcelize
data class User(
val name: String,
val phoneNumber:String
) : Parcelable
2.Then 在您所在的当前可组合项中,例如主屏幕
val userDetails = UserDetails(
name = "emma",
phoneNumber = "1234"
)
)
navController.currentBackStackEntry?.arguments?.apply {
putParcelable("userDetails",userDetails)
}
navController.navigate(Destination.DetailsScreen.route)
3.Then 在 details composable 中,确保将 navcontroller 作为参数传递给它,例如
@Composable
fun Details (navController:NavController){
val data = remember {
mutableStateOf(navController.previousBackStackEntry?.arguments?.getParcelable<UserDetails>("userDetails")!!)
}
}
N.B: 如果 parcelable 没有被传递到状态,你会在返回时收到一个错误
我对 Moshi
的处理方式:
路线
sealed class Route(
private val route: String,
val Key: String = "",
) {
object Main : Route(route = "main")
object Profile : Route(route = "profile", Key = "user")
override fun toString(): String {
return when {
Key.isNotEmpty() -> "$route/{$Key}"
else -> route
}
}
}
扩展
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.core.net.toUri
import androidx.navigation.*
import com.squareup.moshi.Moshi
inline fun <reified T> NavController.navigate(
route: String,
data: Pair<String, T>,
navOptions: NavOptions? = null,
navigatorExtras: Navigator.Extras? = null,
) {
val count = route
.split("{${data.first}}")
.size
.dec()
if (count != 1) {
throw IllegalArgumentException()
}
val out = Moshi.Builder()
.build()
.adapter(T::class.java)
.toJson(data.second)
val newRoute = route.replace(
oldValue = "{${data.first}}",
newValue = Uri.encode(out),
)
navigate(
request = NavDeepLinkRequest.Builder
.fromUri(NavDestination.createRoute(route = newRoute).toUri())
.build(),
navOptions = navOptions,
navigatorExtras = navigatorExtras,
)
}
inline fun <reified T> NavBackStackEntry.getData(key: String): T? {
val data = arguments?.getString(key)
return when {
data != null -> Moshi.Builder()
.build()
.adapter(T::class.java)
.fromJson(data)
else -> null
}
}
@Composable
inline fun <reified T> NavBackStackEntry.rememberGetData(key: String): T? {
return remember { getData<T>(key) }
}
用法示例
data class User(
val id: Int,
val name: String,
)
@Composable
fun RootNavGraph() {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "${Route.Main}",
) {
composable(
route = "${Route.Main}",
) {
Button(
onClick = {
navController.navigate(
route = "${Route.Profile}",
data = Route.Profile.Key to User(id = 1000, name = "John Doe"),
)
},
content = { Text(text = "Go to Profile") },
}
composable(
route = "${Route.Profile}",
arguments = listOf(
navArgument(name = Route.Profile.Key) { type = NavType.StringType },
),
) { entry ->
val user = entry.rememberGetData<User>(key = Route.Profile.Key)
Text(text = "$user")
}
}
}