android-jetpack-composejetpack-compose-navigation

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

Solution

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