Cast Error on passing Integer Nav Argument using SavedStateHandle in Compose - android

I am trying to navigate from the first screen to a second screen and I need to provide an integer identifier to load stuff from API on the second screen.
I'm running into this error when trying to pass an Int Nav Argument.
java.lang.ClassCastException: java.lang.String cannot be cast to java.lang.Integer
This is my NavHost where I use /{recipeId} as my placeholder on the 2nd Screen's route.
NavHost(
navController = navController,
startDestination = Screens.RecipesOverviewScreen.route
) {
//1st Screen
composable(route = Screens.RecipesOverviewScreen.route) {
RecipesOverviewScreen(
navController = navController,
onToggleTheme = { app.toggleLightTheme() })
}
//2nd Screen
composable(route = "${Screens.RecipeDetailScreen.route}/{recipeId}")
{
RecipeDetailScreen()
}
I then call navController.navigate() in the first screen passing in an id of type Int into the navigation route.
RecipeList(recipes = listState.recipes,
onClickRecipeCard = { id ->
//insert corresponding Int id into the Nav route
navController.navigate(
route = "${Screens.RecipeDetailScreen.route}/${id}"
)
}
Inside the 2nd Screen's ViewModel I retrieve the nav argument using the SavedHandleInstance.
#HiltViewModel
class RecipeViewModel #Inject constructor(
savedStateHandle: SavedStateHandle
) : ViewModel() {
init {
//pass in the key inside get() fxn
savedStateHandle.get<Int>(Constants.PARAM_RECIPE_ID)
?.let { id ->
//perform an API call inside init{} using nav arg id
getRecipe(id = id, token = token)
} ....}
At this point, the app is crashing and I am getting the above logcat output.
Kindly point me in the right direction on passing an Int Nav arg.

When you defined your 2nd screen as:
composable(route = "${Screens.RecipeDetailScreen.route}/{recipeId}") {
You didn't define any type for the recipeId argument. As per the Navigate with arguments guide:
By default, all arguments are parsed as strings. You can specify another type by using the arguments parameter to set a type
So if you want your recipeId to be an Int, you must declare it as an IntType:
composable(
route = "${Screens.RecipeDetailScreen.route}/{recipeId}",
arguments = listOf(navArgument("recipeId") { type = NavType.IntType })
) {
This will ensure that your call to savedStateHandle.get<Int> actually has an Int to find, rather than a String.

Related

How to pass an object through Navigation in Android compose?

I am using Navigation Compose, and I am trying to pass an entire object to the next screen, So I am using a shared ViewModel through hilt, and created a mutablestate variable of that object and want to get its value in the next screen.
Like This
var campaign = mutableStateOf<Campaign?>(null)
private set
fun addCampaign(campaign: Campaign){
this.campaign.value = campaign
}
where Campaign is just a data-class.
In my Screen in Navigation
LazyColumn (
content =
{
items(viewModel.campaignListCurrent){ campaign ->
CampaignItem(
image = campaign.brand?.image ?: "",
title = campaign.name ,
id = campaign.id ,
description =campaign.description ,
date =campaign.createdAt ) {
viewModel.addCampaign(
CampaignsViewModel.Campaign(...) // the "campaign" object is used to fill this
)
Timber.tag("CampaignObject").v(viewModel.campaign.value.toString())
viewModel.changed = true
navController.navigate(Screen.CampaignDetailsScreen.route)
}
}
})
I can see when I log the data that it was stored successfully. yet in the next screen when I get the data from the same ViewModel, it's null. even though its a mutablestate and supposed to change its value and be observables, I don't think I get mutablestate behavior at all, and any link for a proper explanation for it will be appreciated.
val campaign by remember { viewModel.campaign }
Timber.v("CampaignDetailsScreen2: " + campaign.toString())
can someone explain to me why it doesn't work here, even if I used Launched effect??
why doesn't change its value here?
I managed to solve this by Creating a SharedViewModel instance in the NavGraph to make sure it is the same Instance used in both composable functions.
#Composable
fun CampaignsScreen(
navController : NavController,
viewModel: CampaignsViewModel
) {
....
#Composable
fun CampaignDetailsScreen(
navController : NavController,
viewModel: CampaignsViewModel
) {
and in my NavGraph I passed the ViewModel as HiltViewmodel in the constructor and from it to the two screens
#Composable
fun SetUpNavGraph (navController : NavHostController,
campaignViewModel: CampaignsViewModel = hiltViewModel()) {
...........
loginNavGraph(navController = navController)
homeNavGraph(navController = navController, campaignsViewModel = campaignViewModel)
}
}
and in the home NavGraph I passed the parametere and its the same Instance in both
fun NavGraphBuilder.homeNavGraph(
navController : NavHostController,
campaignsViewModel: CampaignsViewModel
){
navigation(startDestination = Screen.HomeScreen.route, route = HOME_GRAPH_ROUTE) {
................
composable(Screen.Campaigns.route){
CampaignsScreen(navController = navController, viewModel = campaignsViewModel)
}
composable(Screen.CampaignDetailsScreen.route){
CampaignDetailsScreen(navController, viewModel = campaignsViewModel)
}
}
}
You can use backstackEntry and PreviouseBackStack to solve this but it won't work if you have a JSON Object within your object as you will need to Serialize and Deserialize this object manually and it will take more time and code to achieve the same goal for a shared ViewModel to pass Parclized object. until Google Solve passing Parclized objects in Compose Navigation I recommend backStack Entry if you don't have a JSON object within your paralyzed object and if you do, use a shared ViewModel between the tow composable function.
Why would it work if you are just remembering a local value and not even reading from the proper model?
val campaign = viewModel.campaign
Can't just use remember willy-nilly without understanding the proper usage.

Pass value of ViewModel to a new Composable screen instance

I have a composable function declared like this:
fun ScreenA(
nav: NavController,
type: SomeTypeObject,
) {
val vm = getViewModel<SomeTypeObjectViewModel>()
val state = rememberScaffoldState()
val scope = rememberCoroutineScope()
LaunchedEffect(LocalContext.current) {
when(type) {
SomeTypeObject.TYPE1 ->{
vm.updateState("1")
}
SomeTypeObject.TYPE2 -> {
//do something else
}
}
}
SomeTypeObjectViewModel contains state variable of my ScreenA like this:
var remeberVal = mutableStateOf<SomeTypeObject?>(null)
Now at some point in another composable function i use my navigationGraph to open another instance of ScreenA, so SomeTypeObjectViewModel gets recreated and remeberVal restes istelf but i want keep and reuse it when new instance of ScreenA is made.
Passing remeberVal as argument using the navigationGraph is not an option since you can only pass Strings, ints or parcelable objects which is not my case, considering that remeberVal has MutableState<SomeTypeObject?> type.
At this point my question is:
Is there a way to pass remeberVal to the new instance of ScreenA or to avoid SomeTypeObjectViewModel being reinstantiated after when i re-route to ScreenA using my navigaion graph?
Thank you!
Edit:
my getViewModel() is a Koin function to injevt the ViewModel, the internal code is:
org.koin.androidx.compose ViewModelComposeExtKt.class #Composable
public inline fun <reified T : ViewModel> getViewModel(
qualifier: Qualifier?,
owner: ViewModelStoreOwner,
scope: Scope,
noinline parameters: ParametersDefinition? /* = (() → ParametersHolder)? */
): T
The navigation graph is made in something like this way:
fun MyNGraph(nav: NavHostController) {
composable(
route = Routes.CaseType1.route + "/{someParameters}/",
arguments = listOf(
navArgument("someParameters") {},
),
) { backStackEntry ->
val someParameters = backStackEntry.arguments?.getString("someParameters")
someParameters?.let { someParameters ->
ScreenA(
type = SomeTypeObject.TYPE1, // Notice here, where i change type but use the same screen
)
}
}
}
composable(
route = Routes.CaseType2.route + "/{someParameters}/",
arguments = listOf(
navArgument("someParameters") {},
),
) { backStackEntry ->
val someParameters = backStackEntry.arguments?.getString("someParameters")
someParameters?.let { someParameters ->
ScreenA(
type = SomeTypeObject.TYPE2, // Notice here
)
}
}
}
}
You are using Koin for DI, so you can just add a dependency with a broader scope than your SomeTypeObjectViewModel that will hold the state you want to share between different screens/composables or between different VM instances. In that way your VMs have access to a shared state (a shared state holder is usually called a Repository).
class MySharedState {
// this could also be a MutableState instead of MutableStateFlow
// but then you are spreading the androidx.compose.runtime dependency
// to a shared state that should not need to know about Compose
val typeFlow = MutableStateFlow<SomeTypeObject?>(null)
}
class SomeTypeObjectViewModel(
val sharedState: MySharedState
): ViewModel() {
fun updateType(type: SomeTypeObject) {
sharedState.typeFlow.value = type
}
fun updateState(value: String) {
// your existing logic...
// call updateType(...) when you want to update the type
}
// rest of your ViewModel code
}
Where you are configuring your Koin modules add (if you are using Koin 3.2+)
module {
// a shared state scoped to the whole app lifecycle
singleOf(::MySharedState) // <-- add this
viewModelOf(::SomeTypeObjectViewModel) // <-- you probably already have this
}
If you are using Koin < 3.2
module {
// a shared state scoped to the whole app lifecycle
single { MySharedState() } // <-- add this
viewModel { SomeTypeObjectViewModel(get()) } // <-- you probably already have this but add one more get()
}
If you also want to access the state in your composables, you can use Flow.collectAsState()
fun ScreenA(
nav: NavController,
type: SomeTypeObject,
) {
val vm = getViewModel<SomeTypeObjectViewModel>()
val currentType by vm.sharedState.typeFlow.collectAsState()
// ...
}
by scoping your ViewModel to navigation routes or the navigation graph you can retrieve the same instance of your ViewModel
visit https://developer.android.com/jetpack/compose/libraries#hilt-navigation
#Composable
fun MyApp() {
NavHost(navController, startDestination = startRoute) {
navigation(startDestination = innerStartRoute, route = "Parent") {
// ...
composable("exampleWithRoute") { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry("Parent")
}
val parentViewModel = hiltViewModel<ParentViewModel>(parentEntry)
ExampleWithRouteScreen(parentViewModel)
}
}
}
}

Why can I remove the code navArgument without problem when I use Navigating with arguments in JetPack Compose?

I'm learning Jetpack Compose Navigation.
The Code A is from the article. It works well.
I can't understand fully what the code navArgument means. I find the Code B which is removed the code navArgument can work well too.
What does the code navArgument mean?
Code A
val accountsName = RallyScreen.Accounts.name
NavHost(...) {
...
composable(
"$accountsName/{name}",
arguments = listOf(
navArgument("name") {
// Make argument type safe
type = NavType.StringType
}
)
) { entry -> // Look up "name" in NavBackStackEntry's arguments
val accountName = entry.arguments?.getString("name")
..
}
}
Code B
val accountsName = RallyScreen.Accounts.name
NavHost(...) {
...
composable(
"$accountsName/{name}"
) { entry -> // Look up "name" in NavBackStackEntry's arguments
val accountName = entry.arguments?.getString("name")
...
}
}
As per documentation:
By default, all arguments are parsed as strings. You can specify another type by using the arguments parameter to set a type
So the second option works, and it can be used if all your parameters are of string type.
navArgument should be used when the default string type is not appropriate for your parameters, and you need it to be int, optional string, etc.

Jetpack Compose Navigation - pass argument to startDestination

The app I'm building uses compose navigation with routes. The challenge is that the start destination is dynamic.
Here is a minimal example:
class MainActivity : ComponentActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
startDestination = "dynamic/1", // doesn't work
// startDestination = "static", // workaround
) {
composable(
route = "dynamic/{$ARG_ID}",
arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType }),
) {
val id = it.arguments?.getString(ARG_ID)
Text("dynamic route, received argument: $id!")
}
// part of the workaround
// composable(
// route = "static",
// ) {
// LaunchedEffect(this) {
// navController.navigate("dynamic/1")
// }
// }
}
}
}
companion object
{
const val ARG_ID = "id"
}
}
The app crashes with
java.lang.IllegalArgumentException: navigation destination route/1 is not a direct child of this NavGraph
The problem only exists if the "dynamic" route is used as start destination. This can be verified by using startDestination = "static".
Although, the "static" route workaround works I'm looking for a solution without it because it kind of obfuscates the code and also creates an additional entry in the back stack.
-> Full code sample to reproduce the issue
Related SO questions
Navigation Architecture Component- Passing argument data to the startDestination - Answers don't seem to be applicable to Compose Navigation.
Pass an argument to a nested navigation graph in Jetpack Compose - No answer given.
Compose Navigation - navigation destination ... is not a direct child of this NavGraph - The accepted answer doesn't resolve the issue.
Edit:
I want to stress that the original sample used to not contain the "static" composable. I only added the "static" composable to have a working startDestination and to prove that the "dynamic" composable can be navigated to.
Update:
Even switching to the query parameter syntax for optional arguments, providing a default value, and setting the start destination without any argument does not work.
The following variation
NavHost(
navController = navController,
startDestination = "dynamic",
) {
composable(
route = "dynamic?$ARG_ID={$ARG_ID}",
arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType; defaultValue = "1" }),
) {
val id = it.arguments?.getString(ARG_ID)
Text("dynamic route, received argument: $id!")
}
}
Leads to the exception
java.lang.IllegalArgumentException: navigation destination dynamic is not a direct child of this NavGraph
Full credit goes to ianhanniballake, who explained the solution to me in a comment. I'm going to show the working version of my code sample here.
The big insight to me was:
startDestination must not match a composable route in the sense of pattern matching but it must be exactly the same string.
That means an argument can't be set via startDestination directly but has to be set via the argument's defaultValue.
Here is the working sample:
class MainActivity : ComponentActivity()
{
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(
navController = navController,
// 1st change: Set startDestination to the exact string of route
startDestination = "dynamic/{$ARG_ID}", // NOT "dynamic/1", provide arguments via defaultValue
) {
composable(
route = "dynamic/{$ARG_ID}",
// 2nd change: Set startDestination argument via defaultValue
arguments = listOf(navArgument(ARG_ID) { type = NavType.StringType; defaultValue = "1" }),
) {
val id = it.arguments?.getString(ARG_ID)
Text("dynamic route, received argument: $id!")
}
}
}
}
companion object
{
const val ARG_ID = "id"
}
}
The approach equally works with the argument provided in the form of a query parameter.
To be honest, I see this as a small limitation because the start route now dictates what has to be the defaultValue. I might want to set a different defaultValue or none at all. Yet, in most cases this should be the most elegant solution.
should not be using dynamic route value in "startDestination" NavHost
--> navController.navigate(<dynamic route ‌>)
All credit goes to ianhanniballake and Peter. In my case I didn't add any additional (mandatory key/optional key) in route for the argument data. I kept the route clean like below:
Nav graph:
navigation(route = Route.Root.route, startDestination = Route.SubmitForm.route) {
composable(
route = Route.SubmitForm.route,
arguments = listOf(
navArgument(ARG_KEY) {
type = NavType.StringType
defaultValue = JsonConverter.toJson(user, User::class.java)
},
)
)
}
Route sealed class:
sealed class Route(val route: String) {
object MyRoute : Route("$ROOT/submit-form")
}
And in view model just get the data like this:
#HiltViewModel
class MyViewModel #Inject constructor(
stateHandle: SavedStateHandle,
) : ViewModel {
lateinit var user
init {
user = stateHandle.get<String>(ARG_NAME) // Supported data types
}
}
It worked for me.

Pass an argument to a nested navigation graph in Jetpack Compose

From the docs, I see you can nest navigation graphs like so:
NavHost(navController, startDestination = "home") {
...
// Navigating to the graph via its route ('login') automatically
// navigates to the graph's start destination - 'username'
// therefore encapsulating the graph's internal routing logic
navigation(startDestination = "username", route = "login") {
composable("username") { ... }
composable("password") { ... }
composable("registration") { ... }
}
...
}
I am wondering, how would one pass an argument in the route, and make that available to all composables inside the nav graph?
Here's my current nav graph:
navigation(
// I'd like to grab this parameter
route = "dashboard?classId={classId}",
startDestination = Route.ScreenOne.route) {
composable(Route.ScreenOne.route) {
// And then pass the parameter here, or to any composable below
ScreenOne(classId)
}
composable(Route.ScreenTwo.route) {
ScreenTwo()
}
composable(Route.ScreenThree.route) {
ScreenThree()
}
}
I am basically trying to avoid setting the classId navigation argument individually on each composable route. I didn't see a way to pass a list of arguments to navigation() like you can in a composable().
It might be that what I am describing isn't possible, but looking forward to anyone's thoughts!
You can access the graph arguments from child composables:
navController.getBackStackEntry("dashboard?classId={classId}").arguments?.getString("classId")
From a quick test within a Hilt-based project, it looks like passing a property in as an argument to a navigation graph component results in the property being available in the savedStateHandle for any ViewModels that are made whilst that graph is in memory.
For example:
// 1. Define your routes.
sealed class Destination(val route: String) {
object TestGraph : Destination("TEST_GRAPH/{${Arguments.testParameter}}") {
object Arguments {
const val testParameter = "testParameter"
}
fun route(testParameter: String): String {
return "TEST_GRAPH/$testParameter"
}
object FirstScreen : Destination("FIRST_SCREEN")
}
}
// 2. Create a graph extension on NavGraphBuilder for the navigation graph.
private fun NavGraphBuilder.testGraph(navController: NavHostController) {
navigation(
startDestination = Destination.TestGraph.FirstScreen.route,
route = Destination.TestGraph.route,
arguments = listOf(
navArgument(Destination.TestGraph.Arguments.testParameter) { type = NavType.StringType }
)
) {
composable(route = Destination.TestGraph.FirstScreen.route) {
FirstScreen()
}
}
}
// 3. Use the line below to navigate to this new graph.
navController.navigateTo(Destination.TestGraph.route("xyz"))
// 4. Access the savedStateHandle via the VM to get the parameter.
#Composable
fun FirstScreen(
viewModel: FirstScreenViewModel = hiltViewModel(),
) {
//...
}
#HiltViewModel
class FirstScreenViewModel #Inject constructor(
savedStateHandle: SavedStateHandle,
): ViewModel() {
private val testParameter: String = checkNotNull(savedStateHandle[Destination.TestGraph.Arguments.testParameter])
//...
}
I assume this works as the argument is created and maintained at the navigation graph-level. So, providing the graph is in memory, the property is accessible via savedStateHandle. If you were to pop this graph off of the navigation stack, I would expect the value to not be accessible anymore. Hope that helps!

Categories

Resources