How to handle popping back multiple screens with Jetpack Compose Navigation - android

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

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

Compose navigation - passing arguments to startDestination of nested graph

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.

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?

Jetpack Compose & Navigation: Problems share ViewModel in nested graph

According to this example I implemented shared viewModels in a nested navigation graph.
Setup
Nested Graph:
private fun NavGraphBuilder.accountGraph(navController: NavHostController) {
navigation(
startDestination = "main",
route = "account") {
composable("main") {
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
composable("login") {
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
}
}
NavHost:
#Composable
private fun NavHost(navController: NavHostController, modifier: Modifier = Modifier){
NavHost(
navController = navController,
startDestination = MainScreen.Home.route,
modifier = modifier
) {
composable("home") { HomeScreen(hiltViewModel()) }
composable("otherRoute") { OtherScreen(hiltViewModel()) }
accountGraph(navController)
}
}
BottomNavBar:
#Composable
private fun ButtonNav(navController: NavHostController) {
BottomNavigation {
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = navBackStackEntry?.destination
items.forEach { screen ->
BottomNavigationItem(
icon = { ... },
label = { ... },
selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
onClick = {
navController.navigate(screen.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
navController.graph.startDestinationRoute?.let { route ->
popUpTo(route) { saveState = true }
}
// Avoid multiple copies of the same destination when
// re-selecting the same item
launchSingleTop = true
// Restore state when re-selecting a previously selected item
restoreState = true
}
}
)
}
}
}
Problem
With this setup if I naviagte to "account" (the nested graph) and back to any other route I get the error:
java.lang.IllegalArgumentException: No destination with route account is on the NavController's back stack. The current destination is Destination(0x78dd8526) route=otherRoute
Assumptions / Research Results
BottomNavItem
The exception did not occure when I remove the popUpTo(route) onClick. But then I ended up with a large stack.
lifecycle of backStackEntry
Have a look at the following:
//...
composable("main") { backStackEntry ->
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
//...
I found out when navigating back the composable which will be left will be recomposed but in this case the backStackEntry seams to have another lifecycle.currentState because if I wrap the whole composable like this:
//...
composable("main") { backStackEntry ->
if(backStackEntry.lifecycle.currentState == Lifecycle.State.RESUMED){
val vm = hiltViewModel<AccountViewModel(navController.getBackStackEntry("account"))
//... ui ...
}
}
//...
... the exception did not occure.
The idea with the lifecycle issue came into my mind when I saw that the offical example has similar workarounds in place.
Summary
I actually do not know if I did something wrong or if I miss a conecept here. I can put the lifecycle-check-workaround into place but is this really as intended? Additional to that I did not find any hint in the doc regarding that.
Does anybody know how to fix that in a proper way?
Regards,
Chris
This is how you do it now but make sure you have the latest compose navigation artefacts:
private fun NavGraphBuilder.accountGraph(navController: NavHostController) {
navigation(
startDestination = "main",
route = "account") {
composable("main") {
val parentEntry = remember {
navController.getBackstackEntry("account")
}
val vm = hiltViewModel<AccountViewModel(parentEntry)
//... ui ...
}
composable("login") {
val parentEntry = remember {
navController.getBackstackEntry("account")
}
val vm = hiltViewModel<AccountViewModel(parentEntry)
//... ui ...
}
}
}
There was an issue with the navigation component. It has been fixed for me with v2.4.0-alpha08

Jetpack Compose - Bottom navigation icon is not selected if it has nested navigation

I want to have a bottom navigation bar with two items/screens: Order and Account. Order is the start destination. Order has its own navigation and it has two screens: ItemList and ItemDetail. ItemDetail opens when an item is clicked in ItemList screen.
When I run the app, I can see the ItemList screen but Order item in the bottom navigation bar is not selected. If I click on Account item, I can see Account screen and Account item gets selected in the bottom navigation bar.
I think this is happening because of the recomposition: when Order is selected at the beginning since it is the start destination, its nested graph is called and a new destination (ItemList) is navigated, leading a recomposition, with currentRoute being "itemList" rather than "order".
How can I get Order icon selected in the bottom navigation bar? Is there a recommended what of handling nested graphs with bottom nav?
This is what I have at the moment:
object Destinations {
const val ORDER_ROUTE = "order"
const val ACCOUNT_ROUTE = "account"
const val ITEM_LIST_ROUTE = "itemList"
const val ITEM_DETAIL_ROUTE = "itemDetail"
const val ITEM_DETAIL_ID_KEY = "itemId"
}
class NavigationActions(navController: NavHostController) {
val selectItem: (Long) -> Unit = { itemId: Long ->
navController.navigate("${Destinations.ITEM_DETAIL_ROUTE}/$itemId")
}
val upPress: () -> Unit = {
navController.navigateUp()
}
}
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MyApp()
}
}
}
#Compose
fun MyApp() {
MyAppTheme {
val navController = rememberNavController()
val tabs = listOf(Destinations.ORDER_ROUTE, Destinations.ACCOUNT_ROUTE)
val navBackStackEntry by navController.currentBackStackEntryAsState()
val currentRoute = navBackStackEntry?.arguments?.getString(KEY_ROUTE)
Scaffold(
bottomBar = {
BottomNavigation {
tabs.forEach { tab ->
BottomNavigationItem(
icon = { Icons.Filled.Favorite },
label = { Text(tab) },
selected = currentRoute == tab,
onClick = {
navController.navigate(tab) {
popUpTo = navController.graph.startDestination
launchSingleTop = true
}
},
alwaysShowLabel = true,
selectedContentColor = MaterialTheme.colors.secondary,
unselectedContentColor = LocalContentColor.current
)
}
}
}
) {
NavGraph(navController)
}
}
}
#Composable
fun NavGraph(
navController: NavHostController,
startDestination: String = Destinations.ORDER_ROUTE
) {
val actions = remember(navController) { NavigationActions(navController) }
NavHost(navController = navController, startDestination = startDestination) {
navigation(startDestination = Destinations.ITEM_LIST_ROUTE, route = Destinations.ORDER_ROUTE) {
composable(Destinations.ITEM_LIST_ROUTE) {
ItemList(actions.selectItem)
}
composable(
"${Destinations.ITEM_DETAIL_ROUTE}/{$Destinations.ITEM_DETAIL_ID_KEY}",
arguments = listOf(navArgument(Destinations.ITEM_DETAIL_ID_KEY) {
type = NavType.LongType
})
) {
ItemDetail()
}
}
composable(Destinations.ACCOUNT_ROUTE) {
Account()
}
}
}
I wrote this article with a similar example. It's in Portuguese but if you translate the page to English you'll get the idea... Also, you can find the sources here.
I think the problem is happening because you're using just one NavHost for the entire app. In fact, I guess you need to use one NavHost for each tab, then when the user select a tab, you must change the current NavHost.
oh! my article is based on this post here, which can also help you.

Categories

Resources