Compose navigation - passing arguments to startDestination of nested graph - android

What is the correct way of passing arguments to startDestination of a nested navigation graph? See this example:
private const val featureGraphRoute = "feature_graph"
private const val firstRouteArg = "intArgument"
private const val firstRoute = "first_route/{$firstRouteArg}"
private const val secondRoute = "second_route"
fun NavController.navigateToFeatureGraph(argument:Int, navOptions: NavOptions? = null) {
//TODO: pass the argument
this.navigate(featureGraphRoute, navOptions)
}
fun NavGraphBuilder.featureGraph() {
navigation(
route = featureGraphRoute,
startDestination = firstRoute
) {
composable(
route = firstRoute,
arguments = listOf(
navArgument(firstRouteArg){
type = NavType.IntType
}
)
) { backStackEntry ->
FirstRoute(
argument = backStackEntry.arguments?.getInt(firstRouteArg)
)
}
composable(route = firstRoute) {
SecondRoute()
}
}
}
Adding the same argument to the featureGraphRoute does seem to work but only if using NavType.StringType. Otherwise app crashes with exception:
java.lang.IllegalArgumentException: Wrong argument type for 'intArgument' in argument bundle. integer expected.
EDIT:
I somehow missed the fact that NavGraphBuilder.navigation has an overload that takes arguments. Moving the navArgument declaration up one level does prevent the crash.

Related

java.lang.IllegalArgumentException when navigate with argument in Navigation Android Compose

I'm running into a problem when trying to navigate with argument in my very first compose project
Error:
java.lang.IllegalArgumentException: Navigation destination that matches request NavDeepLinkRequest{ uri=android-app://androidx.navigation/transaction_detail/{1} } cannot be found in the navigation graph NavGraph...
My NavGraph:
#Composable
fun SetupNavGraph(
navController: NavHostController
) {
NavHost(
navController = navController,
startDestination = HomeDestination.route,
) {
composable(route = HomeDestination.route) {
HomeScreen(
navigateToItemEntry = { navController.navigate(TransactionEntryDestination.route) },
navigateToItemUpdate = {
navController.navigate("${TransactionDetailDestination.route}/{$it}")
}
)
}
//detail screen route
composable(
route = TransactionDetailDestination.routeWithArgs,
arguments = listOf(
navArgument(TransactionDetailDestination.transactionIdArg) {
type = NavType.IntType
}
)
) {
val id = it.arguments?.getInt(TransactionDetailDestination.transactionIdArg)!!
TransactionDetailScreen(id)
}
}
}
My transaction detail screen:
object TransactionDetailDestination : NavigationDestination {
override val route = "transaction_detail"
override val title = "Transaction Detail Screen"
const val transactionIdArg = "transactionId"
val routeWithArgs = "$route/{$transactionIdArg}"
}
#Composable
fun TransactionDetailScreen(id: Int) {
Scaffold {
TransactionDetailBody(paddingValues = it, id = id)
}
}
#Composable
fun TransactionDetailBody(
paddingValues: PaddingValues,
id: Int
) {
Column(modifier = Modifier.fillMaxSize()) {
Text(text = "$id", fontSize = 100.sp)
...
}
}
I can see that the problem is the route to transaction detail destination, but I don't know where to correct. I'm looking forward to every suggestion!
By research on internet a lot a realize that when specify the route to go, in my case, always like this:
//'it' is the argument we need to send
//rule: 'route/value1/value2...' where 'value' is what we trying to send over
navController.navigate("${TransactionDetailDestination.route}/$it")
The string of the route we need to extract the argument(s) from:
//notice the naming rule: 'route/{arg1}/{arg2}/...'
val routeWithArgs = "${route}/{${transactionIdArg}}"
Only be doing the above the compiler will understand the argument you are trying to send and receive. My mistake not reading carefully. Hope it helps!
I think you didn't declare your destination argument in your graph like this
composable("transaction_detail/{id}")
according to this documentation

Missing road arguments value while using Navigation in Jetpack Compose

I created a road using Navigation (with Jetpack Compose) as so
composable(CatLifeScreen.AddCatFormScreen.route + "?catId={catId}",
arguments = listOf(
navArgument(name = "catId") {
nullable = true
}
)) {
bottomBarState.value = false
val catId = it.arguments?.getInt("catId")
AddEditCatFormBody(
idOfCat = catId,
navController = navController,
addEditCatViewModel = addEditCatViewModel,
)
If I don't add any argument, the road works good
onClick = { navController.navigate(CatLifeScreen.AddCatFormScreen.route) }
If I define an argument it.arguments?.getInt("catId") is 0 when my passed argument is not 0.
IconButton(onClick = { navController.navigate(CatLifeScreen.AddCatFormScreen.route + "?catId=$idOfCat") } //idOfCat = 1
Does anyone have a lead to follow or something? I've been stuck for a long time now
Finally found the answer.
For anyone stuck like I was:
Put the type of the data you want to receive
Integer value can't be nullable (app will crash if you do).(see documentation https://developer.android.com/guide/navigation/navigation-pass-data#supported_argument_types ). In my case, I don't specify the type
Fetch key in arguments using getString(key) and cast it to Int (only if you don't specify the type because you want it to be nullable. You probably could deal with it differently (like using -1 when you don't have a value to pass, instead of null)
composable(CatLifeScreen.AddCatFormScreen.route + "?catId={catId}",
arguments = listOf(
navArgument(name = "catId") {
nullable = true // I don't specify the type because Integer can't be nullable
}
)) {
bottomBarState.value = false
val catId = it.arguments?.getString("catId")?.toInt() // Fetch key using getString() and cast it
AddEditCatFormBody(
idOfCat = catId,
navController = navController,
addEditCatViewModel = addEditCatViewModel,
)
}
try typing the argument type, like this;
arguments = listOf(
navArgument("catId") {
type = NavType.IntType
nullable = true
}
)

Jetpack Compose Navigation: Direct navigation to route in a nested graph which is not startDestination

I am working on Jetpack Compose Navigation demo and I have a nested navigation graph with two different nested routes and screens for each nested route:
Login Graph
Main Graph
Login Graph has three routes for display three different Screens
Route "login" for displaying LoginScreen
Route "register" for displaying RegisterScreen
Route "recoverPassword" for displaying RecoverPasswordScreen
Main Graph has two routes for these screens
Route "home" for displaying HomeScreen
Route "settings" for displaying SettingsScreen
The nested graph creation is called in the MainActivity.kt
setContent {
NavigationDemoTheme {
val navController = rememberNavController()
SetupNavGraph(navController = navController)
}
}
The function in the file NestedNavGraph.kt looks like this:
fun SetupNavGraph(navController: NavHostController) {
NavHost(navController = navController, startDestination = "login_route")
{
loginGraph(navController = navController)
mainGraph(navController = navController)
}
}
In the file LoginNavGraph.kt I have defined the routes and start destination
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "login", route = "login_route") {
composable(route = "login") {
LoginScreen(navController = navController)
}
composable(route = "register") {
RegisterScreen(navController = navController)
}
composable(route = "recover") {
RecoverPasswordScreen(navController = navController)
}
}
}
In the file MainNavGraph.kt I have defined these two routes and this start destination:
navigation(startDestination = "home", route = "main_route") {
composable(route = "home") {
HomeScreen(navController = navController)
}
composable(route = "settings") {
SettingsScreen(navController = navController)
}
}
My questions now is: How can I display the RecoverPasswordScreen from SettingsScreen. I know I can navigate to the "login_route" from the SettingsScreen with but then the startDestination will be displayed, which is the LoginScreen.
// shows the LoginScreen because the startDestination in the "login_route" is set to "login"
navController.navigate(route = "login_route")
So, how can I directly navigate to the route "recover" in the nested graph route "login_route"? The following "workarounds" are in my mind:
Pass a parameter to the "login_route", for example something with:
navController.navigate(route = "login_route?destination=recover")
I will then have only a single route as a destination, for example "LoginView". This will change the loginGraph like this:
fun NavGraphBuilder.loginGraph(navController: NavController) {
navigation(startDestination = "login_view, route = "login_route/{destination}) {
composable(
route = "login_view",
arguments = listOf(
navArgument("destination") { defaultValue = "login" },
)
) { backStackEntry ->
val destination = backStackEntry.arguments?.getString("destination");
destination?.let { destination ->
LoginView(destination = destination)
}
}
}
}
The LoginView is composable whichw will have a own NavHost where I can set the startDestination with the query parameter from the previous route:
fun LoginView( destination : String = "login"){
val navController = rememberNavController()
var startDestination = destination;
Scaffold ()
{
NavHost(
navController = navController,
startDestination = startDestination
) {
composable(route = "login") {
LoginScreen(navController = navController)
}
composable(route = "register") {
RegisterScreen(navController = navController)
}
composable(route = "recover") {
RecoverPasswordScreen(navController = navController)
}
}
}
Now I should be able to call the RecoverPasswordScreen from the SettingsScreen with this:
navController.navigate(route = "login_route?destination=recover")
Another possibility is to have extra route for the RecoverPassword Screen in the MainGraph defined. Is there any other possibilty to directly acess a route in a nested graph? It would be great if could dynamically change startDestination when routing to "login_route" but I don't know how or if this is even possible.
Compose allows you to (Navigate with arguments). This allows you to navigate to what you are calling "nested routes", that is a specific part within a screen.
Now, this is a simple explanation and I could leave you and have you figure it out. But I don't think this would be helpful to you as I think you have implemented your navigation in a difficult manner. Hence why trying to navigate is a bit more complex.
Here is a better way to implement it so that navigation like the one you want(RecoverPasswordScreen from Settings Screen) is easier.
Disclaimers
Change anything that's referred to as Main to your AppName.
I have not added all your screens
Main Screen class
//you could pass in parameters if needed into this constructor
enum class MainScreen(){
//these are your screens
LogIn(),
Settings(),
Recover(),
Home();
companion object {
fun fromRoute(route: String?): MainScreen =
when (route?.substringBefore("/")) {
LogIn.name -> LogIn
Home.name -> Home
Settings.name -> Settings
Recover.name -> Recover
//add the remaining screens
// a null route resolves to LogInScreen.
null -> LogIn
else -> throw IllegalArgumentException("Route $route is not recognized.")
}
}
}
Main Activity Class
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MainApp()
}
}
}
#Composable
fun MainApp() {
MainTheme {
val allScreens = MainScreen.values().toList()
val navController = rememberNavController()
val backStackEntry = navController.currentBackStackEntryAsState()
// currentScrren user is on good if app is large
val currentScreen = MainScreen.fromRoute(
backStackEntry.value?.destination?.route
)
//Using scaffold is a good idea
Scaffold(
//add topAppBar and all other things here
) { innerPadding ->
MainNavHost(navController = navController, modifier = Modifier.padding(innerPadding))
}
}
}
//Scaffold requires innerPadding so remove if you decide not to use scaffold
#Composable
fun MainNavHost(navController: NavHostController, modifier: Modifier = Modifier) {
NavHost(
navController = navController,
startDestination = LogIn.name,
modifier = modifier
) {
composable(LogIn.name) {
/**
Your body for logIn page
**/
}
//this is how you will navigate to Recover Screen from settings
composable(Settings.name) {
SettingsBody(onClickRecoverScreen = {navController.navigate(Recover.name)})
}
}
composable(Recover.name) {
/**
Your body for Recover page
**/
}
composable(Home.name) {
/**
Your body for Home page
**/
}
}
Settings Screen
#Composable
fun SettingsBody(
//this callback is how you will navigate from Settings to RecoverPassword
onClickRecoverScreen: () -> Unit = {},
) {
Column(
//Add your designs for this screen
) {
Button(onClick = {onClickRecoverScreen})
}
}
This is the simplest way (in my opinion) to implement Navigation as you can simply add callbacks to navigate to different places in the app and it is much more testable(if you test ;) ) and scalable. You can also add deep links and use arguments (as mentioned above) to navigate to specific parts of the app (e.g., a specific account in an Accounts Screen)
I highly recommend this Navigation Codelab if you want to understand more.
A possible solution is to use deeplinks defined in the navigation graph - they also work for nested destinations. Then, instead of navigating to the route name, you can use navController.navigate(deepLinkUri)

Hilt with Jetpack Compose Navigation

I checked this info https://developer.android.com/jetpack/compose/libraries#hilt-navigation how to inject ViewModel to a compose screen.
For now I implemeted like this for my test app:
NavHost(
navController = navController,
startDestination = startDestination,
modifier = modifier
) {
composable(Screen.Topics.name) {
val parentEntry = remember { navController.getBackStackEntry(Screen.Topics.name) }
val topicsViewModel = hiltViewModel<TopicsViewModel>(parentEntry)
TopicsScreen(
topicsViewModel = topicsViewModel,
openDrawer = openDrawer,
navigateToTopicDetails = { topic -> actions.navigateToTopicsDetails(topic) }
)
}
...
Is there will be any difference if I use
val parentEntry = remember { navController.getBackStackEntry(Screen.Topics.name) }
val topicsViewModel = hiltViewModel<TopicsViewModel>(parentEntry)
or just
val topicsViewModel = hiltViewModel<TopicsViewModel>()
I guess first one is needed only if we use nested graphs and we want to get ViewModel for specific graph scope https://developer.android.com/jetpack/compose/navigation#nested-nav
So in my case the scope is the same for both methods if I don't use nested graphs?
So can I just use hiltViewModel<TopicsViewModel>() in my case?

How to handle popping back multiple screens with Jetpack Compose Navigation

I'll try to do some ASCII art to describe the problem:
<--------------------------------------\
DestinationA --> DestinationC ---------> DestinationE
DestinationB ------/ \-----> DestinationD --/
I hope that's decipherable. C can be reached from destinations A and B. E can be reached from C and D. E returns to either A or B (whichever is in the back stack). Destinations C, D, and E take an argument (id).
What is the best way to implement this? Using nested navigation graphs looks like it might be possible.
The following works, but it feels more like a work-around than how the navigation component is intended to work.
val destination = navController.getBackStackEntry("DestinationC/{id}").destination
navController.popBackStack(destination.id, true)
The usage NavHost is currently:
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "DestinationA") {
compose("DestinationA") {
ScreenA(hiltNavGraphViewModel(it))
}
compose("DestinationB") {
ScreenB(hiltNavGraphViewModel(it))
}
compose("DestinationC/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType })) {
val viewModel = hiltNavGraphViewModel(it)
val id = it.arguments?.getString("id")
viewModel.setId(id)
ScreenC(viewModel)
}
compose("DestinationD/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType })) {
val viewModel = hiltNavGraphViewModel(it)
val id = it.arguments?.getString("id")
viewModel.setId(id)
ScreenD(viewModel)
}
compose("DestinationE/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType })) {
val viewModel = hiltNavGraphViewModel(it)
val id = it.arguments?.getString("id")
viewModel.setId(id)
ScreenE(viewModel)
}
}
The answer from #rofie-sagara did not work for me. There is a navigation extension that supports routes. I think nested navigation is an unrelated topic. The docs don't really explain why nested navigation is actually useful. My final solutions to move from E back to A or B is:
navigation.popBackStack(route = "DestinationC/{id}", inclusive = true)
Using nested navigation graphs Make DestinationC and DestinationE on diff navigations.
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "DestinationA") {
compose("DestinationA") {
ScreenA(hiltNavGraphViewModel(it))
}
compose("DestinationB") {
ScreenB(hiltNavGraphViewModel(it))
}
navigation("DestinationC".plus("/{id}"), "DestinationC".plus("_Route")) {
compose("DestinationC/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType })) {
val viewModel = hiltNavGraphViewModel(it)
val id = it.arguments?.getString("id")
viewModel.setId(id)
ScreenC(ViewModel)
}
}
compose("DestinationD/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType })) {
val viewModel = hiltNavGraphViewModel(it)
val id = it.arguments?.getString("id")
viewModel.setId(id)
ScreenD(viewModel)
}
navigation("DestinationE".plus("/{id}"), "DestinationE".plus("_Route")) {
compose("DestinationE/{id}", arguments = listOf(navArgument("id") { type = NavType.StringType })) {
val viewModel = hiltNavGraphViewModel(it)
val id = it.arguments?.getString("id")
viewModel.setId(id)
ScreenE(ViewModel)
}
}
}
example you want to move from C to E and popUpTo A.
navController.navigate("DestinationE".plus("/${data.id}")) {
popUpTo("DestinationA") {
inclusive = false
}
}

Categories

Resources