'Pass Parcelable argument with compose navigation
I want to pass a parcelable object (BluetoothDevice) to a composable using compose navigation.
Passing primitive types is easy:
composable(
"profile/{userId}",
arguments = listOf(navArgument("userId") { type = NavType.StringType })
) {...}
navController.navigate("profile/user1234")
But I can't pass a parcelable object in the route unless I can serialize it to a string.
composable(
"deviceDetails/{device}",
arguments = listOf(navArgument("device") { type = NavType.ParcelableType(BluetoothDevice::class.java) })
) {...}
val device: BluetoothDevice = ...
navController.navigate("deviceDetails/$device")
The code above obviously doesn't work because it just implicitly calls toString().
Is there a way to either serialize a Parcelable to a String so I can pass it in the route or pass the navigation argument as an object with a function other than navigate(route: String)?
Solution 1:[1]
I've written a small extension for the 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)
}
}
As you can check there are at least 16 functions "navigate" with different parameters, so it's just a converter for use
public open fun navigate(@IdRes resId: Int, args: Bundle?)
So using this extension you can use Compose Navigation without these terrible deep link parameters for arguments at routes.
Solution 2:[2]
Here's my version of using the BackStackEntry
Usage:
composable("your_route") { entry ->
AwesomeScreen(entry.requiredArg("your_arg_key"))
}
navController.navigate("your_route", "your_arg_key" to yourArg)
Extensions:
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" }
}
}
Solution 3:[3]
Here is another solution that works also by adding the Parcelable to the correct NavBackStackEntry, NOT the previous entry. The idea is first to call navController.navigate, then add the argument to the last NavBackStackEntry.arguments in the NavController.backQueue. Be mindful that this does use another library group restricted API (annotated with RestrictTo(LIBRARY_GROUP)), so could potentially break. Solutions posted by some others use the restricted NavBackStackEntry.arguments, however NavController.backQueue is also restricted.
Here are some extensions for the NavController for navigating and NavBackStackEntry for retrieving the arguments within the route composable:
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)
}
To navigate with a single argument, you can now do this in the scope of a 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
}
Or if you want to pass multiple arguments of the same type:
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
}
The key benefit of this approach is that similar to the answer that uses Moshi to serialise the argument, it will work when popUpTo is used in the navOptions, but will also be more efficient as no JSON serialisation is involved.
This will of course not work with deep links, but it will survive process or activity recreation. For cases where you need to support deep links or even just optional arguments to navigation routes, you can use the entry.rememberArgument extension. Unlike entry.rememberRequiredArgument, it will return null instead of throwing an IllegalStateException.
Solution 4:[4]
The backStackEntry solution given by @nglauber will not work if we pop up (popUpTo(...)) back stacks on navigate(...).
So here is another solution. We can pass the object by converting it to a JSON string.
Example code:
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
){
// ...
}
Solution 5:[5]
Following nglauber suggestion, I've created two extensions which are helping me a bit
@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)
}
And I use them this way:
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)
Solution 6:[6]
I had a similar issue where I had to pass a string that contains slashes, and since they are used as separators for deep link arguments I could not do that. Escaping them didn't seem "clean" to me.
I came up with the following workaround, which can be easily tweaked for your case. I rewrote NavHost, NavController.createGraph and NavGraphBuilder.composable from androidx.navigation.compose as follows:
@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()
})
}
Where Screen is my destination enum
sealed class Screen {
object Index : Screen()
object Example : Screen()
}
Please note that I removed deep links and arguments since I am not using them. That will still allow me to pass and retrieve arguments manually, and that functionality can be re-added, I simply didn't need it for my case.
Say I want Example to take a string argument path
const val ARG_PATH = "path"
I then initialise the NavHost like so
NavHost(navController, startDestination = Screen.Index) {
composable(Screen.Index) { IndexScreen(::navToExample) }
composable(Screen.Example) { navBackStackEntry ->
navBackStackEntry.arguments?.getString(ARG_PATH)?.let { path ->
ExampleScreen(path, ::navToIndex)
}
}
}
And this is how I navigate to Example passing path
fun navToExample(path: String) {
navController.navigate(Screen.Example.hashCode(), Bundle().apply {
putString(ARG_PATH, path)
})
}
I am sure that this can be improved, but these were my initial thoughts. To enable deep links, you will need to revert back to using
// composable() and other places
val internalRoute = "android-app://androidx.navigation.compose/$route"
id = internalRoute.hashCode()
Solution 7:[7]
Since the nglauber's answer work when going forward and does not when navigating backward and you get a null. I thought maybe at least for the time being we can save the passed argument using remember in our composable and be hopeful that they add the Parcelable argument type to the navigating with the route.
the destination composable target:
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"),
)
}
And here's the the source code: to trigger the navigation:
navController.currentBackStackEntry?.arguments =
Bundle().apply {
putParcelable(PROJECT_ARGUMENT_KEY, project)
}
navController.navigate("yourRout")
Solution 8:[8]
A very simple and basic way to do is as below
1.First create the parcelable object that you want to pass e.g
@Parcelize
data class User(
val name: String,
val phoneNumber:String
) : Parcelable
2.Then in the current composable that you are in e.g main screen
val userDetails = UserDetails(
name = "emma",
phoneNumber = "1234"
)
)
navController.currentBackStackEntry?.arguments?.apply {
putParcelable("userDetails",userDetails)
}
navController.navigate(Destination.DetailsScreen.route)
3.Then in the details composable, make sure you pass to it a navcontroller as an parameter e.g.
@Composable
fun Details (navController:NavController){
val data = remember {
mutableStateOf(navController.previousBackStackEntry?.arguments?.getParcelable<UserDetails>("userDetails")!!)
}
}
N.B: If the parcelable is not passed into state, you will receive an error when navigating back
Solution 9:[9]
My approach with Moshi:
Routes
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
}
}
}
Extension
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) }
}
Example usage
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")
}
}
}
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
| Solution | Source |
|---|---|
| Solution 1 | Valentin Yuryev |
| Solution 2 | |
| Solution 3 | |
| Solution 4 | |
| Solution 5 | Dharman |
| Solution 6 | Aleksandr Belkin |
| Solution 7 | Amin Keshavarzian |
| Solution 8 | |
| Solution 9 |
