How to pass object in navigation in jetpack compose?
Asked Answered
H

7

35

From the documentation, I can pass string, integer etc. But how can I pass objects on navigation?

Note: If I set the argument type parcelable then the app crashes with java.lang.UnsupportedOperationException: Parcelables don't support default values..

composable(
    "vendor/details/{vendor}",
        arguments = listOf(navArgument("vendor") {
            type = NavType.ParcelableType(Vendor::class.java)
        })
) {
// ...
}
Houseless answered 16/4, 2021 at 8:7 Comment(3)
It's not possible to pass a Parcelable object. You could pass an ID instead of an entire object.Violate
I found 2 workaround solutions. Check out the answer: https://mcmap.net/q/431912/-how-to-pass-object-in-navigation-in-jetpack-composeHouseless
Duplicated: #65610503Topple
H
35

As per the Navigation documentation:

Caution: Passing complex data structures over arguments is considered an anti-pattern. Each destination should be responsible for loading UI data based on the minimum necessary information, such as item IDs. This simplifies process recreation and avoids potential data inconsistencies.

So, if it is possible avoid passing complex data. More official details here.


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 following workarounds are based on navigation-compose version 2.7.5.


I found 2 workarounds for passing objects.

1. Convert the object into JSON string

Here we can pass the objects using the JSON string representation of the object.

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?
){
    // ...
}

Important Note: If your data has any URL or any string with & etc., you may have to use URLEncoder.encode(jsonString, "utf-8") and URLDecode.decode(jsonString, "utf-8") for pass and receive data respectively. But encoding-decoding also has some side effects! Like if your string has any + sign, it will replace that with a space etc.

2. Passing the object using NavBackStackEntry

Here we can pass data using navController.currentBackStackEntry and receive data using navController.previousBackStackEntry.

Note: From version 1.6.0 any changes to *BackStackEntry.arguments will not be reflected in subsequent accesses to the arguments. So, now we have to use savedStateHandle. Version change details here.

Example code:

val ROUTE_USER_DETAILS = "user-details"


// Pass data
val user = User(id = 1, name = "John Doe") // User is a parcelable data class.

// `arguments` will not work after version 1.6.0.
// navController.currentBackStackEntry?.arguments?.putParcelable("user", user) // old
snavController.currentBackStackEntry?.savedStateHandle?.set("user", user) // new
navController.navigate(ROUTE_USER_DETAILS)


// Receive data
NavHost {
    composable(ROUTE_USER_DETAILS) {
        // `arguments` will not work after version 1.6.0.
        // val userObject = navController.previousBackStackEntry?.arguments?.getParcelable<User>("user") // old
        val userObject: User? = navController.previousBackStackEntry?.savedStateHandle?.get("user") // new
        
        UserDetailsScreen(userObject) // Here UserDetailsScreen is a composable.
    }
}


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

Important Note: The 2nd solution is unstable and will not work if we pop up back-stacks on navigate.

Houseless answered 17/4, 2021 at 0:11 Comment(6)
Any workarounds on the new version? My app is crashing when i use this! My navigation version is 2.4.0-alpha03Permanency
Use the first solution. The solution is updated. Now using optional arguments.Houseless
First solution also does not work if your data class contains a url. Navigation cannot find it for some reason and causes IllegalArgumentException as i asked here. Beware if your data class contains urls for images for instance.Inerasable
@Inerasable If you using Moshi, after generating JSON string, encode the string using URLEncoder.encode(jsonString, "utf-8"). It should solve the problem. You can also create an extension function like this fun String.urlEncode(): String = URLEncoder.encode(this, "utf-8") and use it like: jsonAdapter.toJson(obj).urlEncode().Houseless
@MahmudulHasanShohag thanks for the solution. It also works with kotlin serializationShier
When using Gson for instance, remember to decode at the receiving end also with URLDecoder.decode(jsonString, StandardCharsets.UTF_8.toString())Soma
E
5

With Arguments:

You can just make this object Serializable and pass it to the backStackEntry arguments, also you can pass String, Long etc :

data class User (val name:String) : java.io.Serializable

val user = User("Bob")

navController.currentBackStackEntry?.arguments?.apply {
        putString("your_key", "key value")
        putSerializable("USER", user)
      )
}

to get value from arguments you need to do next:

navController.previousBackStackEntry?.arguments?.customGetSerializable("USER")

code for customGetSerializable function:

@Suppress("DEPRECATION")
inline fun <reified T : Serializable> Bundle.customGetSerializable(key: String): T? {
    return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) getSerializable(key, T::class.java)
    else getSerializable(key) as? T
}

With savedStateHandle

Sometimes you have nullable arguments, so you can use savedStateHandle:

appState.navController.currentBackStackEntry?.savedStateHandle?.set("USER", user)

and get value:

navController.previousBackStackEntry?.savedStateHandle?.get("USER")
Estriol answered 9/1, 2023 at 17:12 Comment(0)
G
3

Parcelables currently don't support default values so you need to pass your object as String value. Yes it is a work around.. So instead of passing object itself as Parcelize object we can turn that object into JSON (String) and pass it through navigation and then parse that JSON back to Object at destination. You can use GSON for object to json string conversion...

Json To Object

fun <A> String.fromJson(type: Class<A>): A {
    return Gson().fromJson(this, type)
}

Object To Json String

fun <A> A.toJson(): String? {
    return Gson().toJson(this)
}

User NavType.StringType instead of NavType.ParcelableType..

composable("detail/{item}",
            arguments = listOf(navArgument("item") {
                type = NavType.StringType
            })
        ) {
            it.arguments?.getString("item")?.let { jsonString ->
                val user = jsonString.fromJson(User::class.java)
                DetailScreen( navController = navController, user = user )
            }
          }

Now navigate by passing string..

  val userString = user.toJson()
  navController.navigate(detail/$userString")

EDIT: There is also a limit for the Json-String that you can navigate. If the length of the Json-String is tooo long then the NavController won't recognize your Composable Route eventually throw an exception... Another work around would be to use a Global Variable and set its value in before navigating.. then pass this value as arguments in your Composable Functions..

 var globalUser : User? = null // global object somewhere in your code
    .....
    .....
    // While Navigating
    click { user->
            globalUser = user
            navController.navigate(detail/$userString")
          }

    // Composable
    composable( "detail") {
            DetailScreen(
                navController = navController,
                globalUser)
             }

NOTE :-> ViewModels can also be used to achieve this..

Gamecock answered 16/6, 2021 at 20:40 Comment(3)
This solution already given here.Houseless
The only difference in his solution is GSON usage (moshi was used in already given solution).Minetta
How long json string is allowed? I am getting some data trimmed at receiving end :DIreful
F
3

In general this is not a recommended practice to pass objects in Jetpack Compose navigation. It's better to pass data id instead and access that data from repository.

But if you want to go this way I would recommend to use CBOR instead of JSON. It's shorter and you can pass everything, including urls. Kotlin serialization supports it.

@Serializable
data class Vendor(
  val url: String,
  val name: String,
  val timestmap: Long
)

val vendor = Vendor(...)
val serializedVendor = Cbor.encodeToHexString(vendor)

For large objects don't forget to call Cbor.encodeToHexString(vendor) on Dispatchers.Default instead of blocking the main thread.

Fleshings answered 4/3, 2023 at 0:8 Comment(0)
P
0

Let me give you very simple answers. We have different options like.

  1. Using Arguments but issue with this is that you can't share long or complex objects, only simple types like Int, String, etc. Now you are thinking about converting objects to JsonString and trying to pass it, but this trick only works for small or easy objects. Exception look like this:

    java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri="VERY LONG OBJECT STRING" } cannot be found in the navigation graph NavGraph(0x0) startDestination={Destination(0x2e9fc7db) route=Screen_A}

  2. Now we have a Parsable Type in navArgument, but we need to put that object in current backStack and need to retrieve from next screen. The problem with this solution is you need keep that screen in your backStack. You can't PopOut your backStack. Like, if you want to popout your Login Screen when you navigate to Main Screen, then you can't retrieve Object from Login Screen to Main Screen.

  3. You need to Create SharedViewModel. Make sure you only use shared state and only use this technique when above two are not suitable for you.

Pelotas answered 3/7, 2022 at 13:56 Comment(0)
P
0
@HiltViewModel
class JobViewModel : ViewModel() {

 var jobs by mutableStateOf<Job?>(null)
   private set

fun allJob(job:Job)
{
    Toast.makeText(context,"addJob ${job.companyName}", Toast.LENGTH_LONG).show()
    jobs=job
}




 @Composable
fun HomeNavGraph(navController: NavHostController,
 ) {
val jobViewModel:JobViewModel= viewModel() // note:- same jobViewModel pass 
    in argument because instance should be same , otherwise value will null
val context = LocalContext.current
NavHost(
    navController = navController,
    startDestination = NavigationItems.Jobs.route
) {
    composable(
        route = NavigationItems.Jobs.route
    ) {
        JobsScreen(navController,jobViewModel)
    }

    composable(
        route= NavigationItems.JobDescriptionScreen.route
    )
    {
        JobDescriptionScreen(jobViewModel=jobViewModel)
    }

}
}

}

 in function argument (jobViewModel: JobViewModel)
   items(lazyJobItems) {

            job -> Surface(modifier = Modifier.clickable {
                if (job != null) {
                    jobViewModel.allJob(job=job)
                    navController.navigate(NavigationItems.JobDescriptionScreen.route)
                }
Polygnotus answered 8/1, 2023 at 16:46 Comment(0)
I
0

use this extension to pass the bundle:

fun NavController.navigate(
    route: String,
    args: Bundle,
    navOptions: NavOptions? = null,
    navigatorExtras: Navigator.Extras? = null,
) {
    graph
        .findNode(route)
        ?.id
        ?.let { nodeId ->
            navigate(
                resId = nodeId,
                args = args,
                navOptions = navOptions,
                navigatorExtras = navigatorExtras
            )
        }
        ?: error("route $route not found")
}

then just get it like backStackEntry.arguments?.getParcelable(KEY)

Illtimed answered 29/9, 2023 at 10:29 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.