Mahmudul Hasan Shohag
Mahmudul Hasan Shohag

Reputation: 3131

How to pass object in navigation in jetpack compose?

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

Upvotes: 36

Views: 44402

Answers (7)

Mahmudul Hasan Shohag
Mahmudul Hasan Shohag

Reputation: 3131

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.

Upvotes: 43

deviant
deviant

Reputation: 3711

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)

Upvotes: 0

Semyon Tikhonenko
Semyon Tikhonenko

Reputation: 4252

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.

Upvotes: 3

tasjapr
tasjapr

Reputation: 1230

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

Upvotes: 5

UJJAWAL KUMAR
UJJAWAL KUMAR

Reputation: 61

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

Upvotes: 0

Deepak Choudhary
Deepak Choudhary

Reputation: 15

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.

Upvotes: 0

abhi
abhi

Reputation: 1136

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..

Upvotes: 3

Related Questions