I have this scenario in which the user clicks on a button in composable A then selects an item from the list from composable B and selects another item from the list from composable C.
My problem is when I select an item from screen C I want to navigate back to screen A with whatever I selected in B & C. But popBackStack doesn't work when arguments are given.
Here is the code,
navController.popBackStack(route = Screen.SelectPlan.route + "?regionId=${region.id}&operatorId=${operator.id}")
Right now, I see that popBackStack does take a route argument, but converts it to a hashcode to navigate back instead of creating a Uri-like navigate function.
I encountered the same issue and have just discovered the solution for myself.
When you navigate to a destination, using args like so:
val arg1 = "someValue"
val arg2 = "someOtherValue"
navController.navigate("Destination/$arg1/$arg2")
... this route is stored in the newly created backstack entry, not with the values of those args, but with their names as assigned in your NavHost.
Assume my NavHost contains a composable with a route of Destination/{arg1}/{arg2}.
If the .navigate() call in my previous example is executed, an entry will be added to the backstack with this route Destination/{arg1}/{arg2}, not this route Destination/someValue/someOtherValue.
You didn't provide your NavHost in the post, but if you replace the values of those args in the call to .popBackStack() with their names you assign in your NavHost, it should work for you.
Try put "/" instead of ?
navController.popBackStack(route = Screen.SelectPlan.route + "/regionId=${region.id}&operatorId=${operator.id}")
This is indeed a bug and it bothered me for a long time. After a long time of research and reading the navigation source code, I wrote my own extension function NavController.popBackStack to fix this problem. It is tricky but works for me. Also note that this version of extension function does not support the inclusive and saveState options (due to the current navigation API limitation), so you can't restoreState when you return to the current destination.
import android.content.Intent
import androidx.navigation.NavController
fun NavController.popBackStack(route: String): Boolean {
if (backQueue.isEmpty()) {
return false
}
var found = false
var popCount = 0
val iterator = backQueue.reversed().iterator()
while (iterator.hasNext()) {
val entry = iterator.next()
popCount++
val intent = entry
.arguments
?.get("android-support-nav:controller:deepLinkIntent") as Intent?
if (intent?.data?.toString() == "android-app://androidx.navigation/$route") {
found = true
break
}
}
if (found) {
navigate(route) {
while (popCount-- > 0) {
popBackStack()
}
}
}
return found
}
Related
I am using jetpack navigation graph in my android project. Normally to navigate one to another fragment we connect a link between them in nav graph and call like this
findNavController().navigate(R.id.action_navigation_login_to_navigation_send_code)
But We can call the fragment this way as well without creating the link.
findNavController().navigate(R.id.navigation_send_code)
What is the basic difference? In my sense, both do the same work.
Both versions back to the same navController navigate() function that takes in an integer resource id whether it's a fragment id or an action id.
Navigate to a destination from the current navigation graph. This supports both navigating
via an {#link NavDestination#getAction(int) action} and directly navigating to a destination.
Internally it calls this navigate() version where it examines whether it's an action id or not.
If it's an action id, then it gets the destination id from it; if not an action id it consider it as a destination id and continue on; notice the comments in below:
#IdRes int destId = resId;
final NavAction navAction = currentNode.getAction(resId);
Bundle combinedArgs = null;
if (navAction != null) { // here the destId is an action id
if (navOptions == null) {
navOptions = navAction.getNavOptions();
}
destId = navAction.getDestinationId(); // resets the destId to the destination id
Bundle navActionArgs = navAction.getDefaultArguments();
if (navActionArgs != null) {
combinedArgs = new Bundle();
combinedArgs.putAll(navActionArgs);
}
}
// Now destId is the destination fragment id.. continue on...
So, technically no difference, but adding an action id adds an extra step of getting the fragment id which is nothing in terms of scalable apps.
The other difference, that if you put an actionId instead of a fragment id, you'd have an extra feature of navOptions that can be returned from the navGraph like adding enter/exit animation.
I am displaying a paginated list using paging library in a lazy column in a bottom nav bar with 4 tabs say HomeScreen.
When its item is clicked it will to next activity with normal intent . But when i press back key and come back to the composable which has the paginated list . it starts recomposing and the list is shown from the first. How to retain the state of the composable when coming back from other activity on stack.
Only when clicking on a paginated list item and navigating and press back key to revisit HomeScreen the composable with paging list is recomposing and the list is show from first,
other tabs with noraml lazy list is working perfectly on back key revisit.
Please help maintain the list state when revisiting the screen when come to Home screen back from Details page
// getting paged data inside composable
val pagedList = viewModel.getPagedList().collectAsLazyPagingItems()
//navigating on click item
val context = LocalContext.current
context.openActivity(
DetailsScreen::class.java,
extras = {
putInt("id", 5)
})
//open activity extension
fun <T> Context.openActivity(
it: Class<T>,
shouldFinish: Boolean = false,
extras: Bundle.() -> Unit = {},
isOutsideActivity: Boolean = false
) {
val intent = Intent(this, it)
if (isOutsideActivity) {
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
}
intent.putExtras(Bundle().apply(extras))
startActivity(intent)
if (shouldFinish)
this.findActivity()?.let {
it.finish()
}
}
I have some non-optimal app architecture and need to navigate from one fragment to another but it's not clear what is the current destination and arriving.
The simple way is to use something like this:
if (findNavController().currentDestination?.id == R.id.fragmentA)
findNavController().navigate (R.id.action_fragmentA_to_fragmentB)
but how can I make a navigation path dynamically if I know only fragmentB's name / id? Something like
fun navigate(arriveFragment) =
findNavController().navigate (R.id.action_$currentFragment_to_$arriveFragment)
you can navigate using the fragment id
findNavController().navigate(R.id.fragmentB)
navigate() also accept navArguments and navOptions
val args = bundleOf(
"key" to "value"
)
val options = navOptions {
popUpTo(R.id.fragmentA)
}
findNavController.navigate(R.id.fragmentB, args, options)
I'm trying to find if a fragment is already in the backStack of my NavHostFragment (so it automatically manages the fragment transactions and backstack), in order to pop back to it when the user selects that destination from my Side Menu, instead of adding another new fragment to the backstack.
Here's the catch: Many of my fragments are the same Class, let's call it ArticleListFragment, and their contentId param (a simple string id) changes what is being displayed in those fragments.
This means I cannot use nav_host_fragment.childFragmentManager.findFragmentById() since multiple of my fragments in the backstack have the same fragment id.
What I've tried so far is this
var foundIndex = -1
for (i in 0 until nav_host_fragment.childFragmentManager.backStackEntryCount) {
val currFragmentTag = nav_host_fragment.childFragmentManager.getBackStackEntryAt(i).name
val currFragmentId = nav_host_fragment.childFragmentManager.getBackStackEntryAt(i).id
//val currFragment = nav_host_fragment.childFragmentManager.findFragmentByTag(currFragmentTag) // always returns null
val currFragment = nav_host_fragment.childFragmentManager.findFragmentById(currFragmentId) // always returns null
// currentFragment is null so the check always fails
if (currFragment is ArticleListFragment && currFragment.contentId == "a value I need to check") {
foundIndex = i
break
}
}
Am I doing something wrong? Is there another way to check if a fragment, added by the Android Navigation Component, is already in the back stack?
Well, I had the same issue, i can not say that this is a perfect solution but you can get it with
findNavController()
.backStack
.firstOrNull { it.destination.id == ID_YOU_WANT_TO_CHECK }
backstack is actually restricted so the method needs to be Suppressed.
#SuppressLint("RestrictedApi")
I am working with navigation component and bottom navigation
val navController = indNavController(R.id.nav_host_fragment)
bottom_navigation.inflateMenu(R.menu.bottom_navigation_menu)
bottom_navigation.setupWithNavController(navController)
and I am facing the next issue:
When an item is selected in the bottom navigation, then a fragment is loaded. The problem comes when I press again the same item, then a new fragment will be loaded, which it does not make sense at all.
Example:
1- User selects menu item A, then FragmentA is loaded.
2- User selects again menu item A, then a new FragmentA will be loaded,
I was trying to use
bottom_navigation.setOnNavigationItemSelectedListener { }
But then the bottom navigation will not work with the navController.
So the question is: there is a way to handle this situation in order to load again a new fragment when the user is in that screen already?
Finally, I was able to fix this issue.
As I said before, this code:
bottom_navigation.setupWithNavController(navController)
is using
bottom_navigation.setOnNavigationItemSelectedListener { }
so every time I select / reselect an item, the navController will load a new fragment. I checked the javadoc for setOnNavigationItemSelectedListener() and it says:
Set a listener that will be notified when a bottom navigation item is selected. This listener * will also be notified when the
currently selected item is reselected, unless an {#link *
OnNavigationItemReselectedListener} has also been set.
So what I did is to add the next line to my code:
bottom_navigation.setOnNavigationItemReselectedListener { }
and that's all. The navController will load a fragment when an item is selected but not when an item is reselected in the bottom navigation.
I prefer to use the listener from navController:
navController.addOnDestinationChangedListener {
controller, destination, arguments ->
//destination.id for id fragment
}
So the listener is triggered when the destination changes - not by clicking bottom_navigation.
Because setOnNavigationItemSelectedListener is already used when setupWithNavController is declared.
Try this to ignore the user's click on the same selected item:
bottom_navigation.apply {
setOnNavigationItemSelectedListener {
if (it.itemId == bottom_navigation.selectedItemId) {
true
} else when (it.itemId) { ... }
when you use bottom_navigation.setOnNavigationItemSelectedListener { } before bottom_navigation.setupWithNavController(navController) the OnNavigationItemSelectedListener is overrided inside setupWithNavController function. So use
navController.addOnDestinationChangedListener {
controller, destination, arguments ->
//destination.id for id fragment
}