How to kill all fragments created by a NavHostFragment? - android

I have 2 navigation files, and in my Activity, 2 fragments. One of the navigations is always shown inside one of the fragments, but I show the other one only when I need it.
The way they're drawn is the always showing fragment is inside a relativeLayout, and the other fragment is inside the same relativeLayout with it's visibility set as gone. When I need the second navigation, I set the visibility to visible and when I don't need it, I set it to gone again.Visually this works well, but what I want to accomplish is that when I don't want the second navigation, I want to completely kill it and redraw it the next time I need it.
What I've done so far was to get a hold of the NavHostFragment used to start the navigation, and when I dont need it anymore, call popBackStack() on it's navController, but it doesn't work:
val navHost: NavHostFragment? = null
fun createSecondNav() {
navHostLogin = NavHostFragment.create(R.navigation.navigation_second)
theFragment.visibility = View.VISIBLE
supportFragmentManager.beginTransaction()
.replace(R.id.theFragment, navHostLogin!!)
.commit()
}
fun killSecondNav() {
theFragment.visibility = View.GONE
navHostLogin?.navController?.popBackStack() // returns false
navHostLogin = null
}
So how can I completely kill the fragments created by the second navHost?

NavController maintains it's own back-stack, independent form the FragmentManager back-stack.
And popBackStack() without arguments only pops that back-stack once:
Attempts to pop the controller's back stack. Analogous to when the user presses the system Back button when the associated navigation host has focus.
While popBackStack(int destinationId, boolean inclusive) reads:
Attempts to pop the controller's back stack back to a specific destination.
destinationId int: The topmost destination to retain
inclusive boolean: Whether the given destination should also be popped.
So this should be:
navController.popBackStack(R.id.startDestination, true)
I'd wonder why even using two NavController, because one can set the graph at run-time with setGraph(NavGraph graph, Bundle startDestinationArgs):
Sets the navigation graph to the specified graph.
Any current navigation graph data (including back stack) will be replaced.

Related

Android - NavGraph without a startDestination

How can I have a navGraph without a startDestination?
I have a BottomSheet fragment container. When some events are triggered I want to expand this BottomSheet and load a fragment to its fragment container. Until one of those events are triggered I don't want the fragment container to have any fragment loaded. I want it to be empty.
But if don't supply a valid startDestination to the navGraph the app crashes with the exception:
IllegalStateException: no start destination defined
Is this possible? What would be the best way to handle this?
EDIT
I know I can supply a startDestination programmatically. But this is not a graceful approach. It introduces clutter code that needs to be executed when the first fragment load needs to happen and it gets worse when that fragment needs arguments.
The goal is to skip supplying a startDestination altogether.
First solution:
Create empty fragment and set it as startDestination in graph
Second solution:
Wait until event triggered and open BottomSheet with startDestination argument. Read argument of BottomSheet and change start destination
navGraph.setStartDestination(R.id.fragment2)
Set a default startDestination in your navgraph. For me, for example, it is the destination I usually want to start with (R.id.trips_fragment)
Then, in my activity onCreate:
private fun setupMainNavigationGraph() {
val navController = findNavHostFragment().navController
val mainNavigationGraph = navController.navInflater.inflate(R.navigation.main_graph)
mainNavigationGraph.setStartDestination(intent.getIntExtra(START_DESTINATION_INTENT_EXTRA, R.id.trips_fragment))
navController.graph = mainNavigationGraph
}
This way, if I want to use a custom start destination, I pass the start destination ID as an Intent to the activity, and the graph is loaded with the overridden start destination.
Note, the line here
intent.getIntExtra(START_DESTINATION_INTENT_EXTRA, R.id.trips_fragment)
R.id.trips_fragment is the default value.
I use this for integration tests to override the start destination.

Navigation Controller (Managing Backstack) Jetpack Android

Good day. So I've been working around with NavComponent of Jetpack for Android
I've thought that management of BackStack of fragments had to be implemented there already, well in fact it is there but I have faced an issue.
Here is my structure:
I have and entry Activity
I have a NavHost in the activity
I have Bottom Navigation bar in the Activity
For each Bottom Item I am using separate Fragments to navigate through.
Here is the code for the navigation.
bottomNavigationView.setOnNavigationItemSelectedListener {
when (it.itemId) {
R.id.navigation_home -> {
navController.apply {
navigate(R.id.navigation_home)
}
true
}
R.id.navigation_dashboard -> {
navController.apply {
navigate(R.id.dashboardFragment)
}
true
}
R.id.navigation_notifications -> {
true
}
else -> {
false
}
}
}
Never mind the last item.
So the issue is next.
If I try to switch between home and dashboard multiple times, when I press back then the stack surely will start popping all the items included there. So if I move like 6 times it will take me 12 attempts to actually exit the app.
Currently I couldn't find any source where for example the navigate() method will accept some sort of argument to cash my fragments instead of recreating it each time and adding to the BackStack.
So what kind of approach would you suggest?
If I to manage the BackStack manually on each back button pressed, what's the purpose of NavController at all? Just for creating and FORWARD navigation?
I think I'm missing some source in Android's official docs.
Thank you beforehand.
P.S.
using navController.popBackStack() before calling navigate() surely isn't the correct choice.
According to the documentation here :
NavigationUI can also handle bottom navigation. When a user selects a menu item, the NavController calls onNavDestinationSelected() and automatically updates the selected item in the bottom navigation bar.
to do so you have to give your bottom navigation items an ids as same as the corresponding destination in your navigation graph , and then tie you bottom view to the controller like this :
NavHostFragment navHostFragment = supportFragmentManager.findFragmentById(R.id.nav_host_fragment);
NavController navController = navHostFragment.getNavController();
BottomNavigationView bottomNav = findViewById(R.id.bottom_nav);
NavigationUI.setupWithNavController(bottomNav, navController);
Note : from my personal experience , when the startDestination in the graph , that start by default is not currently in back stack (In my case it was the landing page which i pop it out when going to home fragment) then the app act with weird behavior like this . so make sure the start destination is existed in your back stack on should work fine .

OnBackPress On Fragment

I am working on android application that contains five fragment on an activity, What I want is as the fragment 1 is opened and I back-press it comes to Main fragment and same as I press back-press from fragment 5 it also comes to Main fragment.
and When I press on Backpress from MainFragment, the App should Exit.
I have Gone through this link Link
and I have also added the Dispatcher but It not met my requirement.
Like I am always opening each fragment like this
private fun ShowQRCodeFragment() {
val newFragment: Fragment = QrCodeScanningFragment()
val transaction1: FragmentTransaction = supportFragmentManager.beginTransaction()
transaction1.replace(R.id.frameLayout, newFragment)
transaction1.addToBackStack(null)
transaction1.commit()
}
Updated the transaction
private fun FunctionNewSettings() {
val newFragment: Fragment = CustomSettingsFragment()
val transaction1: FragmentTransaction = supportFragmentManager.beginTransaction()
transaction1.replace(R.id.frameLayout, newFragment)
transaction1.addToBackStack("namedata")
fragmentManager.popBackStack()
transaction1.commit()
}
You should use addToBackStack() while fragment transaction. This will allow you to go to the previous fragment on back-press.
For the app exit case, check if the current fragment is MainFragment with the help of fragment tag and calling fragmentmanager.popBackStack() or super.onBackPressed() accordingly.
In MainFragment, use
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(
true // default to enabled
) {
override fun handleOnBackPressed() {
requireActivity().finish()
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, // LifecycleOwner
callback
)
}
In another fragments, use
override fun onAttach(context: Context) {
super.onAttach(context)
val callback = object : OnBackPressedCallback(
true // default to enabled
) {
override fun handleOnBackPressed() {
for (i in 0 until (requireActivity() as FragmentActivity).supportFragmentManager.backStackEntryCount) {
activity.supportFragmentManager.popBackStack()
}
}
}
requireActivity().onBackPressedDispatcher.addCallback(
this, // LifecycleOwner
callback
)
}
if u want to go back to the previous fragment first use
addToBackStack()
and if you want to exit the app/activity by using onBackPressed from activity then in MainFragment use
getActivity().onBackPressed();
if you want to finish the activity from Fragment use
getActivity().finish();
You can also replace existing fragment when user clicks Back button using
fragmentTransaction.replace(int containerViewId, Fragment fragment, String tag)
Solution
Override onBackPressed() method inside your activity.
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count > 1) {
repeat(count - 1) { supportFragmentManager.popBackStack() }
} else {
finish()
}
}
You don't need to mess around with the back button behaviour if you're just switching fragments around, and you shouldn't need to pop the backstack either.
The backstack is just a history, like the back button on your browser. You start with some initial state, like an empty container layout. There's no history before this (nothing on the backstack), so if you hit back now, it will back out of the Activity completely.
If you start a fragment transaction where you add a new fragment to that container, you can use addToBackStack to create a new "step" in the history. So it becomes
empty container -> added fragment
and if you hit back it takes a step back (pops the most recent state off the stack)
empty container
if you don't use addToBackStack, the change replaces the current state on the top of the stack
(with addToBackStack)
empty container -> fragmentA -> fragmentB
(without it)
empty container -> fragmentB
so usually you'll skip adding to the backstack when you add your first fragment, since you don't want an earlier step with the empty container - you want to replace that state
empty container
(add mainFragment without adding the transaction to the backstack)
mainFragment
and now when you're at that first state showing mainFragment, the back button will back out of the activity
So addToBackStack makes changes that are added to the history, and you can step back through each change. Skipping it basically alters the last change instead of making a new one. You can think of it like adding to the backstack is going down a level, so when you hit back you go back up to the previous level. Skipping the add keeps you on the same level, and just changes what you're looking at - hitting back still takes you up a level.
So you can use this to organise the "path" the back button takes, by adding new steps to the stack or changing the current one. If you can write out the stack you want, where the back button takes you back a step each time, you can create it!
One last thing - addToBackStack takes a String? argument, which is usually null, but you can pass in a label for the step you're adding. This allows you to pop the backstack all the way back to a certain point in the history, which is like when a browser lets you jump to the previous site in the history, and not just the last page.
So you can add a name for the transaction, like "show subfragment" when you're adding your first subfragment on top of mainFragment, meaning you can use popBackstack with that label to jump straight to the initial mainFragment state, where the next back press exits the activity. This is way more convenient than popping each step off the backstack, and keeping track of how many you need to do - you can just jump back in the history to a defined point

How do you return to the previous fragment when using a NavController?

I am using a navigation graph to navigate Fragments on Android. Currently, I can go from one fragment to another and return on a back press.
Now I need to return to the previous Fragment in an OnClickListener imbedded in a FloatingActionButton hosted in an Activity that hosts the Fragment instead of a back press.
Currently, the following code takes me all the way to the beginning fragment:
NavController navController = NavHostFragment.findNavController(FragmentSelect.this);
navController.setGraph(R.navigation.nav_graph, bundle);
navController.popBackStack();
I have also tried getActivity().onBackPressed(); which also takes me back to the beginning fragment.
How do you return to the previous Fragment? I am trying to follow this guide,
Pass data to the start destination, for returning a Bundle to a start destination, but it leaves out how to return to a start destination.
I also thought about creating another action to return to the start destination, but then the back button will return to the wrong Fragment. Is there a way to stop that?
If all that you want to do is pop the back stack, setGraph() is unnecessary. Just call popBackStack() on your NavController.

FragmentScenario and nested NavHostFragments don't perform navigations as expected in Instrumentation tests

I am writing a single Activity app that uses Android's Navigation Components to help with navigation and Fragment Scenario for instrumentation testing. I have run into a performance discrepancy when using the back button between the actual app navigation behavior and the behavior of a Fragment being tested in isolation during an Instrumentation tests when using fragment scenario.
In my MainActivity I have a main NavHostFragment that takes up the entire screen. I use that nav host fragment to show several screens including some master detail fragments. Each master detail fragment has another NavHostFragment in it to show the different detail fragments for that feature. This setup works great and provides the behavior I desire.
To accomplish the master detail screen I use a ParentFragment that has two FrameLayouts to create the split screen for tablet and for handset I programatically hide one of the FrameLayouts. When the ParentFragment is created, it detects if it is being run on a tablet or handset and then programatically adds a NavHostFragment to the right frame layout on tablet, and on handset hides the right pane adds a NavHostFragment to the left pane. The NavHostFragments also have a different navigation graph set on them depending on if they are being run on tablet or handset (on handset we show fragments as dialogs, on tablet we show them as regular fragments).
private fun setupTabletView() {
viewDataBinding.framelayoutLeftPane.visibility = View.VISIBLE
if (navHostFragment == null) {
navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_tablet)
navHostFragment?.let {
childFragmentManager.beginTransaction()
.add(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
.setPrimaryNavigationFragment(it)
.commit()
}
}
if (childFragmentManager.findFragmentByTag(SummaryFragment.TAG) == null) {
childFragmentManager.beginTransaction()
.add(R.id.framelayout_right_pane, fragFactory.instantiate(ClassLoader.getSystemClassLoader(), SummaryFragment::class.java.canonicalName!!), SummaryFragment.TAG)
.commit()
}
}
private fun setupPhoneView() {
viewDataBinding.framelayoutLeftPane.visibility = View.GONE
if (navHostFragment == null) {
navHostFragment = NavHostFragment.create(R.navigation.transport_destinations_phone)
navHostFragment?.let {
childFragmentManager.beginTransaction()
.replace(R.id.framelayout_left_pane, it, TRANSPORT_NAV_HOST_TAG)
.setPrimaryNavigationFragment(it)
.commit()
}
}
}
When running the devDebug version of the app, everything works as expected. I am able to navigate using the main NavHostFragment to different master-detail screens. After I navigate to the master-detail screen, the nested NavHostFragment takes over and I can navigate screens in and out of the master detail fragment using the nested NavHostFragment.
When the user attempts to click the back button, which would cause the to leave the master detail screen and navigate to the previous screen, we pop up a dialog to the user asking if they really want to leave the screen (it's a screen where they enter a lot of data). To accomplish this we register an onBackPressDispatcher callback so we know when the back button was pressed and navigate to the dialog when the callback is invoked. In the devDebug version, the user begins by being at location A on the nav graph. If, when they are at location A, they click the back button, then we show a dialog fragment asking if the user really intends to leave the screen. If, instead, the user navigates from location A to location B and clicks back they are first navigated back to location A. If they click the back button again, the back press dispatcher callback is invoked and they are then shown the dialog fragment asking if they really intent to leave location A. So it seems that that the back button affects the back stack of the nested NavHostFragment until the nested NavHostFragment only has one fragment left. When only one fragment is left and the back button is clicked, the onBackPressDisapatcher callback is invoked. This is exactly the desired behavior. However, when I write an Instrumentation test with Fragment Scenario where I attempt to test the ParentFragment I have found that the back press behavior is different. In the test I use Fragment Scenario to launch ParentFragment, I then run a test where I do a navigation in the nested NavHostFragment. When I click the back button I expect that the nested nav host fragment will pop its stack. However, the onBackPressDispatcher callback is invoked immediately instead of after the nested nav host fragment has one fragment left on its stack.
I set some breakpoints in the NavHostFragment and it seems that when the tests are run, the NavHostFragment is not setup to intercept back clicks. Its enableOnBackPressed() method is always called with a flag set to false.
I don't understand what about the test setup is causing this behavior. I would think that the nav host fragment would intercept the back clicks itself until it only had one fragment left on its backstack and only then would the onBackPressDispatcher callback be invoked.
Am I misunderstanding how I should be testing this? Why does the onBackPressDispatcher's callback get called when the back button is pressed.
As seen in the FragmentScenario source code, it does not currently (as of Fragment 1.2.1) use setPrimaryNavigationFragment(). This means that the Fragment being tested does not intercept the back button and hence, its child fragments (such as your NavHostFragment) do not intercept the back button.
You can set this flag yourself in your test:
#Test
fun testParentFragment() {
// Use the reified Kotlin extension to launchFragmentInContainer
with(launchFragmentInContainer<ParentFragment>()) {
onFragment { fragment ->
// Use the fragment-ktx commitNow Kotlin extension
fragment.parentFragmentManager.commitNow {
setPrimaryNavigationFragment(fragment)
}
}
// Now you can proceed with your test
}

Categories

Resources