Removing a fragment from backstack with a dynamic navGraph startDestination - android

I have an Activity with a navGraph that conditionally chooses a startDestination when the the Activity is created.
private suspend fun checkStatusAndNavigate(userID: Int) {
val navController = findNavController(nav_host_container)
val user = getUserFromDB(userID)
val dateNow = Date((if (prefSaved) prefTime() else System.currentTimeMillis())
val navGraph = navController.navInflater.inflate(nav_graph).apply {
// NavGraph inflates with one of three possible starting points: A, B, or C.
startDestination = when {
checkDate(dateNow) == DateCheck.OldDate -> R.id.fragment_a
!user.hasOptedIn -> R.id.fragment_b
else -> R.id.fragment_c
}
}
navController.setGraph(navGraph)
}
After navController.setGraph(navGraph), the startDestination is launched. This works as expected.
Fragment B's only destination out is to Fragment C. But when the user is in Fragment C and does onBackPressed(), I would like to skip Fragment B, and return to whatever called the Activity (there are a few options, so overriding onBackPressed isn't a great option).
I don't think this is a popTo or popToInclusive problem. I don't want to clear the backstack. I just want to keep Fragment B from ever entering the backstack if at all possible.

If you don't want B to be accessible on back from C, then you're supposed to create an Action that is popTo=B, popToInclusive=true, and destination=C.

Related

multi navGraph BottomNavigationView in navigation 2.4.0

I have 3 tabs A, B, C in BottomNavigationView and each has a nav graph.
I can do what I want perfectly in navigation 2.3.3 by a complicated navigation extension, just like the old architecture-components-samples. This sample is now upgrade to 2.4.0. which use less code.
What I want is:
Step: Graph A' s start destination fragment A1 navigate to A2.
Step: Tap tab B or C.
B or C' s navigateUp action is back to A. (works fine)
When back to tab A, it shows A2. (in 2.4.0' s sample, shows A1)
The BottomNavigationView' s ItemReselected action is popBackStack to current graph' s startDestination. (how to set setOnItemReselectedListener if the navController does not change?)
The three startDestination fragment A1, B1, C1 are top level destinations, so B1 and C1' s toolbar do not show back icon. (works fine, because the sample set a set of these 3 fragments instead of navController.graph to AppBarConfiguration)
2.4.0 said it support Multiple back stacks. What does it mean? Can I make my BottomNavigationView in 2.4.0?
Here is how I do "3. The BottomNavigationView' s ItemReselected action..." in 2.3.3:
private fun BottomNavigationView.setupItemReselected(
graphIdToTagMap: SparseArray<String>,
fragmentManager: FragmentManager
) {
setOnNavigationItemReselectedListener { item ->
... // get the item' s navController
navController.popBackStack(
navController.graph.startDestination, false
)
}
}
What I do in 2.4.0:
just copy the sample code.
Add setOnItemReselectedListener and popBackStack
import android.util.SparseIntArray
import androidx.core.util.getOrElse
private val startDestinationIdByNavId: SparseIntArray by lazy(NONE) {
SparseIntArray(5).apply {
put(R.id.tab_home_nav, R.id.tabHomeFragment)
put(R.id.tab_profile_nav, R.id.tabMyProfileFragment)
}
}
fun setupViews() {
binding.bottomNavView.run {
setupWithNavController(navController)
// Pop the back stack to the start destination of the current navController graph
setOnItemReselectedListener {
navController.popBackStack(
destinationId = startDestinationIdByNavId.getOrElse(it.itemId) {
error("Unknown menu item $it")
},
inclusive = false,
)
}
}
}

Navigate to Existing Fragment Using NavController

I have a complicated structure where a number of Fragments link to each other, so using NavController I am trying to avoid creating multiple duplicates of the same Fragment in the BackStack.
I found this post How to check Navigation Destination is in the NavController back stack or not?, which I have implemented as below:
private fun onSiteItemClicked(item: SiteObject) {
Log.d(TAG, "onItemClicked() - ${item.siteReference}")
item.siteID.let {
businessViewModel.updateCurrentSiteVMLiveData(it)
try {
val backStackEntry : NavBackStackEntry = navController.getBackStackEntry(R.id.siteFragment)
makeToast("backStackEntry = ${backStackEntry.destination.label.toString()}")
// Navigate to existing Fragment!! HOW TO DO?
} catch (ex: IllegalArgumentException) {
navController.navigate(R.id.action_contactFragment_to_siteFragment)
// Creates new Fragment as one doesn't existing in backstack. THIS WORKS!
}
}
}
So I have a reference to the Fragment in the BackStack, but I can't see how to navigate to it..

How to change the defaultNavHost among a few already created fragments?

I have a couple of fragments in my activity, each having their own Navigation Graph, they're created when the activity is created. What I want to know is how can I choose any of them to be the defaultNavHost or the PrimaryNavigationFragment after they are created?
The code below creates them and sets them as the default one, but I don't want to recreate them each time. How can I do that?
val finalHost = NavHostFragment.create(navigation)
supportFragmentManager.beginTransaction()
.replace(R.id.navFragment0, finalHost)
.setPrimaryNavigationFragment(finalHost) // this is the equivalent to app:defaultNavHost="true"
.commit()
Couldn't find the code to do what I wanted, so here's my workaround:
First remove the #setPrimaryNavigationFragment(finalHost) from fragmentManager's transaction in your Code, and/or remove app:defaultNavHost="true" from the <fragment...> in your XML.
Then create your NavHostFragments and add each of them to a HashMap:
private val navHostFragments = HashMap<Int, NavHostFragment?>()
fun createNavHosts() {
val finalHost: NavHostFragment =
NavHostFragment.create(navigationId)
.also { navHostFragments[aHandleToTheNavHost] = it }
}
Finally in onBackPressed get the navController of the proper navHost and navigate up on it:
override fun onBackPressed() {
if (navHostFragments[aHandleToTheNavHost]?.navController?.navigateUp() == false)
super.onBackPressed()
}
Note: when #navigateUp() returns false, it means there was nowhere to navigate up to. So in there you can finish the activity or anything else you may want to do.

Navigation's back stack is lost when navigation is nested

I have a navigation which looks like this
Frag1 -> Frag2 -> Frag3
Inside Frag2 there is a NavHostFragment with its own navigation
InnerFrag1 -> InnerFrag2
If I do this
Navigate to Frag2
Navigate to InnerFrag2 inside Frag2
Navigate to Frag3
Go back
then I'll see InnerFrag2 inside Frag2, when I press back normally I would go from InnerFrag2 to InnerFrag1 inside Frag2 but now it's going to Frag1 instead.
Here is my navigation handling inside Frag2
private val backPressedCallback = OnBackPressedCallback {
navHostFragment.navController.navigateUp()
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
requireActivity().addOnBackPressedCallback(backPressedCallback)
}
override fun onDestroyView() {
activity?.removeOnBackPressedCallback(backPressedCallback)
super.onDestroyView()
}
private val navHostFragment: NavHostFragment
get() = childFragmentManager.findFragmentById(R.id.innerNavHostFragment) as NavHostFragment
When going back to Frag2 the fragment in the nav host is the correct one, but navigating back moves away from Frag2 because inner nav host's back stack is lost. Can I persist it somehow or fix it some other way?
EDIT: actually when going from Frag3 to Frag2 I see InnerFrag1 inside, the both look alike, that's why going back at this point brings me back to Frag1
EDIT2: I found my problem, I inflate Frag2s navigation from code in onViewCreated like this
val navHostFragment = (frag2NavHostFragment as? NavHostFragment) ?: return
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
setting it in xml makes it work, I still need to set it from code somehow, Frag2 chooses which navigation to use depending on its arguments
Now my question changes from Navigation's back stack is lost to How to preserve NavHostFragment's state when settings it's graph from code
You can now handle onBackPress on fragments. In your fragment just add this in onViewCreated method.
val navController = Navigation.findNavController(view)
requireActivity().onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
navController.popBackStack(R.id.fragmentWhereYouWantToGo, false)
}
})
I would also give a check to app:popUpTo , app:popUpToInclusive or singleTop XML attributes to the fragments inside your Frag2
After looking into this for a little, original question doesn't make much sense, I'd delete it but it got 2 upvotes ¯\_(ツ)_/¯
I solved my problem by adding a check before inflating graph, so that NavHostFragment's graph is set only if it doesn't already have one.
try {
navHostFragment.navController.graph
} catch (e: IllegalStateException) {
val inflater = navHostFragment.navController.navInflater
val graph = inflater.inflate(navigationId)
navHostFragment.navController.graph = graph
}
NavController.getGraph doesn't return null, instead it throws IllegalStateException, hence the weird check

button back in startDestination with navigation component

I need a second activity with a nav graph and have a return button in toolbar to the first activity that also contains a nav graph
In my second activity I have onSupportNavigateUp and setupActionBarWithNavController when entering the fragments if the arrow back button appears but in the activity no.
Try adding setHomeButtonEnabled and setDisplayHomeAsUpEnabled in both the activity and the fragment and if the button appears back, but when I enter some fragment in front and return to the fragment startDestination disappears the button back
I just need to keep the button back in the activity and solve my problem
You can do it by specifying a setFallbackOnNavigateUpListener:
private fun setupToolbar() {
val navController = findNavController(R.id.nav_host_fragment)
val appBarConfiguration =
AppBarConfiguration.Builder()
.setFallbackOnNavigateUpListener { onNavigateUp() }
.build()
dataBinding.toolbar.apply {
setupWithNavController(navController, appBarConfiguration)
}
}
And then do whatever you want in the Activity's:
override fun onNavigateUp(): Boolean {
finish()
return true
}
You can't, activity has their own toolbars and in your case they have two different NavControllers. So your second activity manage NavUp Button for his fragment and when start Destination fragment comes NavUpButton(Backbutton) disappear because it has no destination left behind. And if you programmatically show NavUp Button on start destination of that (2nd activity) and manage onClick and start first activity that always goes to Start destination of first activity's fragment because it has it's own Nav Controller.
Problem is that Navigation UI not works like that. The better approach is use only one activity with multiple fragments. And use any other approach to solve your problem within the same nav controller.
Add setHomeButtonEnabled func. to your returning action. If you are returning with button add it to onClick or with backPress, override backPress.
With this solve : You will set your button enable, when you try to return your startDestination.
I created an interface to show/hide up button from the nav host activity. Here is how the activity implements the interface methods to show/hide up button:
override fun showUpButton() {
val navController = this.findNavController(R.id.nav_host)
val listener = AppBarConfiguration.OnNavigateUpListener { navController.navigateUp() }
val abc = AppBarConfiguration.Builder().setFallbackOnNavigateUpListener(listener).build()
NavigationUI.setupActionBarWithNavController(this, navController, abc)
}
override fun hideUpButton() {
val navController = this.findNavController(R.id.nav_host)
NavigationUI.setupActionBarWithNavController(this, navController)
}
Here the method in the activity when up button pressed:
override fun onSupportNavigateUp(): Boolean {
val navController = this.findNavController(R.id.nav_host)
if(!navController.navigateUp()){ // When in start destination
onBackPressed()
}
return navController.navigateUp()
}
In a fragment can listen whenever back button (NOT up button) pressed:
private fun setupBackPress() {
requireActivity()
.onBackPressedDispatcher
.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
}
})
}

Categories

Resources