Pass Parcelable argument with compose navigation
Asked Answered
M

17

58

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

Mortie answered 7/1, 2021 at 9:48 Comment(1)
you can JSON serialize your object to String then backAcceleration
C
70

Warning: Ian Lake is an Android Developer Advocate and he says in this answer that pass complex data structures is an anti-pattern (referring the documentation). He works on this library, so he has authority on this. Use the approach below by your own.

Edit: Updated to Compose Navigation 2.4.0-beta07

Seems like previous solution is not supported anymore. Now you need to create a custom NavType.

Let's say you have a class like:

@Parcelize
data class Device(val id: String, val name: String) : Parcelable

Then you need to define a 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)
    }
}

Notice that I'm using Gson to convert the object to a JSON string. But you can use the conversor that you prefer...

Then declare your composable like this:

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

Original answer

Basically you can do the following:

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

And in the details screen...

val device = navController.previousBackStackEntry
    ?.arguments?.getParcelable<BluetoothDevice>("bt_device")
Customhouse answered 7/1, 2021 at 20:34 Comment(15)
For anyone using this solution note the difference between currentBackStackEntry and previousBackStackEntry on either side of the navigate call. Otherwise you'll waste 20 mins looking for your missing data... ;0)Detroit
Did you check if this approach works across process death?Acceleration
This solution will not work if we pop up (popUpTo(...)) back stacks on navigate(...).Either
@Customhouse It works, but after that if you try to navigate again with different button for example without puting a parcelable, you'll get the old parcelable. So the way around would be puting a null parcelable.Anodyne
Note that this won't survive process death. issuetracker.google.com/issues/182194894#comment6Tews
This does not work for me: with androidx.navigation:navigation-compose:2.4.0-alpha01 navController.currentBackStackEntry?.arguments the value of arguments is null for me and it is val therefore cannot be reassignedCocoa
Wait, this basically overwrites the current route's / screen's args, and the next screen looks them up after navigation not from itself, but from the parent. This feels like a hack. What happens when the system needs the original args the parent got for whatever?Burgundy
People don't parse your params to JSON, it is not performant. it's even on the main thread :pulling_my_hear:Fireplug
@Cocoa use this navController.currentBackStackEntry?.arguments?.putParcelable("key", value)Leninakan
It is a bad practice. Despite on I celebrate all efforts to make it possible, it is discouraged by Android design. Here is the response with details from of one of Google|Android engineers with reference to the official documentation. #69059649Roice
all of that looks uglySokil
@Detroit that's why it's ugly and it's better not use itSokil
the app is lagging and freezing when I add navArgument("device") { type = AssetParamType() }Sokil
Do NOT use navController.currentBackStackEntry?.arguments?.putParcelable("key", value). If the currentbackstackEntry doesnt have any arguments already, nothing will happen! @MohammadHosseinKalantarianEmmuela
you can use backEntry.savedStateHandle, backEntry.arguments is blocked from reassigningSokil
I
37

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.

Immersion answered 19/8, 2021 at 11:30 Comment(8)
This should be the accepted answer.Fireplug
Looks nice but I get a nullpointer when I try it.Lions
Best answer. You are much better than googler.Whitesmith
Can you improve it and how to use it?Bradfield
Please specify how can we can get data on next composable where we are heading towards?Cr
Best answer. Hint: Don't use arguments = listOf() in the destination it will cause a crash if you're passing Parcelable or Serializable data typesMoulin
Cant upvote enough 🙏Ri
does it survive process death?Aberration
I
12

I've come up with this solution:

fun NavController.navigate(
    route: String,
    args: Bundle,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null
) {
    val nodeId = graph.findNode(route = route)?.id
    if (nodeId != null) {
        navigate(nodeId, args, navOptions, navigatorExtras)
    }
}

This identifies the registered route by ID in the nav graph, completely bypasses the deep link mechanism and falls back to the original approach.

Register your route in the NavGraphBuilder like this:

composable(
    route = "MyRoute",
) {
    MyScreen(
        navController = navController,
        myArgument = it.arguments?.getParcelable("myParcelableArg")
    )
}

And navigate like this:

navController.navigate(
    route = "MyRoute",
    args = bundleOf(
        "myParcelableArg" to MyParcelable()
    )
)
Interruption answered 28/5, 2023 at 3:9 Comment(0)
F
7

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" }
    }
}
Fireplug answered 5/10, 2021 at 15:34 Comment(0)
C
7
  1. use NavGraph#findNode to get the destination
  2. use destination's id to navigate, the method has a args: Bundle? parameter
val destination = navController.graph.findNode("YOUR_ROUTE")
if (destination != null) {
  navController.navigate(
  destination.id,
  bundleOf("key" to value)
  )
}
Chilopod answered 25/4, 2023 at 12:28 Comment(1)
save time a lot time. thank youNorbertonorbie
S
5

you can pass an argument like this

val data = DetailScreenArgument(title = "Sample")

navController.currentBackStackEntry?.savedStateHandle?.apply {
   set("detailArgument", data)
}

navController.navigate(Screen.DetailScreen.route)

and get the argument in the destination like this

val detailArgument = navController.previousBackStackEntry?.savedStateHandle?.get<DetailScreenArgument>(
    "detailArgument"
)
Scabies answered 27/6, 2022 at 7:34 Comment(5)
java.lang.IllegalArgumentException: Can't put value with type class TranslateModel into saved stateSyrup
what do you want to do?Scabies
I want to pass Custom Object to another composable using navigation graphSyrup
is your data class parcelable?Scabies
Yes, I have already make it parcelableSyrup
U
4

I get a weird bug when I implement the top answer to this question. Like I have the following Parcelable to pass between two screens of my Jetpack Compose app:

@Parcelize
data class EdgeParcelable(
    val node: NodeParcelable?,
    val cursor: String?,
) : Parcelable

And as the accepted Answer says I have implemented a custom NavType:

class EdgeParcelableType : NavType<EdgeParcelable>(isNullableAllowed = false) {

    override val name: String
        get() = "edge"

    override fun get(bundle: Bundle, key: String): EdgeParcelable? {
        return bundle.getParcelable(key)
    }

    override fun parseValue(value: String): EdgeParcelable {
        return Gson().fromJson(value, EdgeParcelable::class.java)
    }

    override fun put(bundle: Bundle, key: String, value: EdgeParcelable) {
        bundle.putParcelable(key, value)
    }
}

And in my Composable function where I create the NavHost I have:

@Composable
fun MyApp(viewModel: MyViewModel, modifier: Modifier = Modifier) {
    val navController = rememberNavController()
    Scaffold(
        modifier = modifier.fillMaxSize(),
        topBar = { MyTopAppBar(
            currentScreen=currentScreen,
            canNavigateBack = navController.previousBackStackEntry != null,
            navigateUp = { navController.navigateUp() }
        ) }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(innerPadding)
        ) {
            composable(route = "home") {
                val lazyPagingItems = viewModel.Items()
                HomeScreen(
                    lazyPagingItems = lazyPagingItems,
                    onTownClicked = { edge: EdgeParcelable ->
                        val json = Uri.encode(Gson().toJson(edgeParcelable))
                      
                     navController.navigateSingleTopTo("hotspots/$json")
                    },
                    modifier = ...
                )
            }
            composable(
                route = "hotspots/{edge}",
                arguments = listOf(
                    navArgument("edge") {
                        type = EdgeParcelableType()
                    }
                )
            ) {
                val edgeParcelable = it.arguments?.getParcelable<EdgeParcelable>("edge")
                HotspotsScreen(edgeParcelable)
            }
        }
    }
}

The code above crashes my Application when I add the lines:

val bsEntry by navController.currentBackStackEntryAsState()
val currentScreen = bsEntry?.destination?.route ?: "Home"

Adding the above lines make the Composable become:

@Composable
fun MyApp(viewModel: MyViewModel, modifier: Modifier = Modifier) {
    val navController = rememberNavController()

    // Adding these causes a problem...
    val bsEntry by navController.currentBackStackEntryAsState()
    val currentScreen = bsEntry?.destination?.route ?: "Home"

    Scaffold(
        modifier = modifier.fillMaxSize(),
        topBar = { MyTopAppBar(
            currentScreen=currentScreen,
            canNavigateBack = navController.previousBackStackEntry != null,
            navigateUp = { navController.navigateUp() }
        ) }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(innerPadding)
        ) {
            composable(route = "home") {
                val lazyPagingItems = viewModel.Items()
                HomeScreen(
                    lazyPagingItems = lazyPagingItems,
                    onTownClicked = { edge: EdgeParcelable ->
                        val json = Uri.encode(Gson().toJson(edgeParcelable))
                     navController.navigateSingleTopTo("hotspots/$json")
                    },
                    modifier = ...
                )
            }
            composable(
                route = "hotspots/{edge}",
                arguments = listOf(
                    navArgument("edge") {
                        type = EdgeParcelableType()
                    }
                )
            ) {
                val edgeParcelable = it.arguments?.getParcelable<EdgeParcelable>("edge")
                HotspotsScreen(edgeParcelable)
            }
        }
    }
}

Passing my Custom NavType with the following line of code:

arguments = listOf(navArgument("edge") { type = EdgeParcelableType() } )

now crashes my app, by rendering it unusable. The app seems to choke on itself, almost like, the Navigation API does not really understand the new Custom EdgeParcleableType() or perhaps something is missing that remains to be added to make this EdgeParcelableType work well with the Navigation API.

I was only able to solve the problem by changing the type above to StringType as follows:

arguments = listOf( navArgument("edge") { type = NavType.StringType }

And passing around strings in the rest of the Composable as follows:

@Composable
fun MyApp(viewModel: MyViewModel, modifier: Modifier = Modifier) {
    val navController = rememberNavController()

    // Using NavType.StringType allows this work...
    val bsEntry by navController.currentBackStackEntryAsState()
    val currentScreen = bsEntry?.destination?.route ?: "Home"

    Scaffold(
        modifier = modifier.fillMaxSize(),
        topBar = { MyTopAppBar(
            currentScreen=currentScreen,
            canNavigateBack = navController.previousBackStackEntry != null,
            navigateUp = { navController.navigateUp() }
        ) }
    ) { innerPadding ->
        NavHost(
            navController = navController,
            startDestination = "home",
            modifier = Modifier.padding(innerPadding)
        ) {
            composable(route = "home") {
                val lazyPagingItems = viewModel.Items()
                HomeScreen(
                    lazyPagingItems = lazyPagingItems,
                    onTownClicked = { edge: EdgeParcelable ->
                        val json = Uri.encode(Gson().toJson(edgeParcelable))
                     navController.navigateSingleTopTo("hotspots/$json")
                    },
                    modifier = ...
                )
            }
            composable(
                route = "hotspots/{edge}",
                arguments = listOf( navArgument("edge") {
                        type = NavType.StringType
                    }
                )
            ) {
                val edgeParcelable = Gson().fromJson(Uri.decode(it.arguments?.getString("edge")), EdgeParcelable::class.java)
                HotspotsScreen(edgeParcelable)
            }
        }
    }
}

Then my app worked like a Charm. Sorting this took me like 2 days of trial and error and searching so I hope this can help someone out there if faced with a similar issue...

Unstick answered 15/11, 2022 at 7:48 Comment(1)
I had the same issue, the only fix is to use object and not class issuetracker.google.com/issues/245406008#comment3Thyrotoxicosis
E
3

Update: New type safety system introduced in Navigation 2.8.0-alpha08

Now you can pass any complex data using Kotlin Serialization officially. Here are some example code:

// Route

@Serializable
data class User(
    val id: Int,
    val name: String
)


// Pass data

navController.navigate(
    User(id = 1, name = "John Doe")
)


// Receive Data

NavHost {
    composable<User> { backStackEntry ->
        val user: User = backStackEntry.toRoute()

        UserDetailsScreen(user) // Here UserDetailsScreen is a composable.
    }
}


// Composable view

@Composable
fun UserDetailsScreen(
    user: User
){
    // ...
}

For more information check out the official blog post from here.


Previous workarounds

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)

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


// Composable function/view
@Composable
fun UserDetailsScreen(
    user: User
){
    // ...
}
Either answered 18/4, 2021 at 10:21 Comment(0)
G
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.

Gefen answered 23/6, 2021 at 14:16 Comment(2)
addedEntry.arguments = it cannot be reassigned, maybe use putParceable??Lions
This broke after a new version of compose navigation was released (it used to be var not val). The problem with using putParcelable is that it requires the arguments field not to be null (it's nullable). I've had to put a temporary hack in to set it through reflection if it's null. I recommend finding another solution. Either find another navigation library that's less dogmatic about forcing developers to model nav routes as URLs, or conform to the dogmatism forced on us by the team behind this and restructure your code to pass IDs to nav routes, then lookup the data you need using IDs.Gefen
N
3

1 - Add the name of the data sent in the route

const val NEWS_DETAILS = "NEWS_DETAILS/{article}/{name}"

2 - Create dataClassParcelable

@Parcelize
data class ArticlesModel(
    @SerializedName("author")
    val author: String?,
    @SerializedName("content")
    val content: String?,
    @SerializedName("description")
    val description: String?,
    @SerializedName("publishedAt")
    val publishedAt: String?,
    @SerializedName("source")
    val source: Strins?,
    @SerializedName("title")
    val title: String?,
    @SerializedName("url")
    val url: String?,
    @SerializedName("urlToImage")
    val urlToImage: String?
):Parcelable

3 - Submit the required data

    fun NavController.openNewsDetails(article: ArticlesModel,name:String) {
        currentBackStackEntry?.savedStateHandle?.apply{

             set(
                 "article",
                 article
             )

             set(
                 "name",
                 name
             )
         }

        navigate(ScreenConst.NEWS_DETAILS)
    }

4 - Reading information and sending them to a new screen

          composable(
                    route = ScreenConst.NEWS_DETAILS
                ) { navBackStackEntry ->
            
                    val article = 
navController.previousBackStackEntry?.savedStateHandle?.get<ArticlesModel("article")
                    val name=
navController.previousBackStackEntry?.savedStateHandle?.get<String>("name")
            
                    if (article==null && name.isNullOrEmpty()) {
                        return@composable
                    }
            
                    DetailsScreen(
                        article = article,
                        name = name
                    )
            
                }
Neoplasty answered 1/11, 2022 at 8:24 Comment(0)
E
2

Following code works for me for passing Parcelable argument with compose navigation:

composable(BottomNavigationScreens.SocialNetwork.route) {
...
onNavigateToRecipeDetailScreen = {
                                    navController.currentBackStackEntry?.savedStateHandle?.set("recipe", it)
                                    navController.navigate(BottomNavigationScreens.RecipeDetails.route)
                                }
}

composable(BottomNavigationScreens.RecipeDetails.route) { backStackEntry ->
                        val recipe = navController.previousBackStackEntry?.savedStateHandle?.get<Recipe>("recipe")
...
}

In this approach, you set the Recipe object in the savedStateHandle of the current back stack entry using savedStateHandle?.set("recipe", it) and then navigate to the "recipe_detail" destination using navController.navigate(BottomNavigationScreens.RecipeDetails.route)

navController.previousBackStackEntry?.savedStateHandle?.get<Recipe>("recipe") to get the Recipe object.

Ecclesiastic answered 29/7, 2023 at 15:8 Comment(0)
E
1

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

Easing answered 12/2, 2022 at 15:55 Comment(1)
looks very uglySokil
I
1

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")
        }
    }
}
Illhumored answered 5/5, 2022 at 16:26 Comment(0)
P
1

You can use my own solution: https://github.com/usmonie/compose_navigation_with_parcelable_arguments

With my solution, you will be able to get parcelable values directly into the composable function

Psf answered 18/10, 2022 at 17:58 Comment(1)
Your answer could be improved with additional supporting information. Please edit to add further details, such as citations or documentation, so that others can confirm that your answer is correct. You can find more information on how to write good answers in the help center.Eyewitness
M
0

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()
Malapropos answered 25/1, 2021 at 3:28 Comment(0)
C
0

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)
Cabral answered 7/4, 2021 at 13:21 Comment(1)
previousBackStackEntry?.arguments? => arguments is nullProsthodontist
B
0

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")
Bruno answered 28/8, 2021 at 13:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.