27

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)
        })
) {
// ...
}
Mahmudul Hasan Shohag
  • 2,203
  • 1
  • 22
  • 30

6 Answers6

24

The following workarounds based on navigation-compose version 2.4.0-alpha05.


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)

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


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

2. Passing the object using NavBackStackEntry:

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

Example code:

val ROUTE_USER_DETAILS = "user-details/{user}"


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

navController.currentBackStackEntry?.arguments?.putParcelable("user", user)
navController.navigate(ROUTE_USER_DETAILS)


// Receive data
NavHost {
    composable(ROUTE_USER_DETAILS) { backStackEntry ->
        val userObject = navController.previousBackStackEntry?.arguments?.getParcelable<User>("user")
        
        UserDetailsView(userObject) // Here UserDetailsView is a composable.
    }
}


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

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

Mahmudul Hasan Shohag
  • 2,203
  • 1
  • 22
  • 30
  • Any workarounds on the new version? My app is crashing when i use this! My navigation version is 2.4.0-alpha03 – MoeinDeveloper Jun 24 '21 at 08:54
  • Use the first solution. The solution is updated. Now using optional arguments. – Mahmudul Hasan Shohag Aug 05 '21 at 09:06
  • 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](https://stackoverflow.com/questions/69176689/jetpack-compose-passing-encoded-string-to-navcontroller-as-argument-causes-illeg). Beware if your data class contains urls for images for instance. – Thracian Sep 14 '21 at 14:11
  • 1
    @Thracian 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()`. – Mahmudul Hasan Shohag Sep 14 '21 at 20:06
  • @MahmudulHasanShohag thanks for the solution. It also works with kotlin serialization – Joseph Ofem Feb 26 '23 at 22:38
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..

abhi
  • 961
  • 9
  • 13
3

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")
tasjapr
  • 632
  • 4
  • 13
2

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.

Semyon Tikhonenko
  • 3,872
  • 6
  • 36
  • 61
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.

Jeremy Caney
  • 7,102
  • 69
  • 48
  • 77
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)
                }
UJJAWAL KUMAR
  • 31
  • 1
  • 6