Reputation: 3131
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
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.
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.
The following workarounds are based on navigation-compose
version 2.7.5
.
I found 2 workarounds for passing objects.
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.
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 thearguments
. So, now we have to usesavedStateHandle
. 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
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
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
Reputation: 1230
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
}
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
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
Reputation: 15
Let me give you very simple answers. We have different options like.
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}
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.
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
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