How to prevent BottomSheetDialogFragment from dismissing after a navigation to another fragment? - android

I am using NavigationComponent on my App.
I have an specific flow where after a click on a button of BottomSheetDialogFragment the app should navigate to another fragment. But when that Fragment is popped I need to navigate back to the previous BottomSheetDialogFragment.
For some reason the BottomSheetDialogFragment is being dismissed automatically.
Frag A : click on a button
Frag A -> Dialog B : click on a button
Frag A -> Dialog B -> Frag C : pop Frag C from the stack
Frag A : Dialog B was automatically dismissed =;/
How can one prevent that dismissing?
Q: Why do I need the BottomSheetDialogFragment not dismissed?
A: I listen to the result of the opened fragment through a LiveData. Due to the dismissing of the BottomSheetDialogFragment it never receives the result.

This is not possible. Dialog destinations implement the FloatingWindow interface which states:
Destinations that implement this interface will automatically be popped off the back stack when you navigate to a new destination.
So it is expected that dialog destinations are automatically popped off the back stack when you navigate to a <fragment> destination. This is not the case when navigating between multiple dialog destinations (those can be stacked on top of one another).
This issue explains a bit more about the limitations here, namely that:
Dialogs are separate windows that always sit above your activity's window. This means that the dialog will continue to intercept the system back button no matter what state the underlying FragmentManager is in or what FragmentTransactions you do.
Operations on the fragment container (i.e., your normal destinations) don't affect dialog fragments. Same if you do FragmentTransactions on a nested FragmentManager.
So once you navigate to your <fragment> destination, the only way for the system back button to actually work is for all floating windows to be popped (otherwise they would intercept the back button before anything else) as those windows are always floating above the content.
This isn't a limitation imposed by the Navigation Component - the same issues apply to any usages of BottomSheetDialogFragment regarding the Fragment back stack and the system back button.

This is not possible as pointed out by #ianhanniballake.
But this can be achieved by making fragment C as a DailogFragment, not a normal Fragment, but this requires some effort to make it behave like a normal fragment.
In this case both B & C are dialogs and therefore they'll share the same back stack. and hence when the back stack is poped up to back from C to B, you'll still see the BottomSheetDialgFragment B showing.
To fix the limited window of C use the below theme:
<style name="DialogTheme" parent="Theme.MyApp">
<item name="android:windowNoTitle">true</item>
<item name="android:windowFullscreen">false</item>
<item name="android:windowIsFloating">false</item>
</style>
Where Theme.MyApp is your app's theme.
And then apply it to C by overriding getTheme():
class FragmentC : DialogFragment() {
//.....
override fun getTheme(): Int = R.style.DialogTheme
}
Also you need to change C in the navigation graph from a fragment to a dialog:
<dialog
android:id="#+id/fragmentC"
android:name="....">
</dialog>
Preview:

You wouldn't want not to dismiss a dialog because it would stay on top of the next destination.
By "listen to result", if you mean findNavController().currentBackStackEntry.savedStateHandle.getLiveData(MY_KEY)
then you should be able to set your result to previousBackStackEntry as it will give you the destination before your dialog.
Frag A : click on a button
Frag A -> Dialog B : click on a button (automatically popped-off)
Frag A -> Dialog B -> Frag C : pop Frag C from the stack
then
class FragA : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
findNavController().currentBackStackEntry.savedStateHandle?.getLiveData<MyResult>(MY_KEY).observe(viewLifecycleOwner) {
// get your result here
// show Dialog B again if you like ?
}
}
}
and
class FragC : Fragment() {
...
private fun setResultAndFinish(result: MyResult) {
findNavController().apply {
previousBackStackEntry?.savedStateHandle?.set(MY_KEY, result)
popBackStack()
}
}
}

Related

Press back to go base fragment with kotlin

I have two fragments; HomeFragment & NextFragment and I go to the next one with a button from the base fragment. Then via back press I want to go back to the first one, however I end up at the apps home screen. A solution
requireActivity().onBackPressedDispatcher
.addCallback(viewLifecycleOwner, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
findNavController().popBackStack()
}
})
was given here but it did not work. I am using ViewPager2 and saw that the home fragment is destroyed when I go to the next one. How can I solve the problem?

Problem going back from current Fragment back to the replaced Fragment when using NavHostFragment

I have three fragments A, B and C. And I'm using navHostFragment container in MainActivity. So the application goes from A -> B using kotlin extension function findNavController().navigate... and then go from B to C using same function. All works fine till here.
Now in Fragment C, I'm replacing different elements on fragment C using
activity?.supportFragmentManager
?.beginTransaction()
?.replace(R.id.list_container, someFragment)
?.addToBackStack("some_frag_id")
?.commit()
The list_container is replaced with someFragment. After this when I press physical back button Fragment C pops out and my app goes to Fragment B while what I expect it to restore replaced list_container i.e. whatever was there before replacement.
I'm also overiding this in my MainActivity
override fun onBackPressed() {
val count = supportFragmentManager.backStackEntryCount
if (count == 0) {
super.onBackPressed()
//additional code
}
else {
supportFragmentManager.popBackStack()
}
}
I'm not sure what is missing here. I have read a lot of solutions on stackoverflow but none worked to my satisfaction. Please guide.
If you are adding a Fragment to a View within a Fragment, you must always use the childFragmentManager - using activity?.supportFragmentManager is always the wrong FragmentManager to use in that case.
Besides fixing cases with restoring state (which would not work when using the wrong FragmentManager), this also ensures that the default behavior for dispatching onBackPressed() down the FragmentManager hierarchy will work out the box - you should not need any logic at all in onBackPressed() to have the pop work correctly.
If you need to intercept the back button in Fragment C, you should follow the providing custom back documentation to register an OnBackPressedDispatcher - you should not override onBackPressed() even in those cases.

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
}

why my progress bar from my activity doesn't show in my fragment after I press hardware back button?

so I am using navigation controller component in Android. I have a progress bar in my MainActivity that will be used in all my fragments when the user need to wait while fetching data from server.
in my onCreate MainActivity it will be declared like this:
progressBar = findViewById(R.id.progressBar_main_activity)
and in my FragmentA, it will be declared like this :
lateinit var mActivity : FragmentActivity
lateinit var progressBar : ProgressBar
override fun onAttach(context: Context) {
super.onAttach(context)
activity?.let {
mActivity = it
}
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,savedInstanceState: Bundle?): View? {
fragmentView = inflater.inflate(R.layout.fragment_user_control, container, false)
progressBar = mActivity.progressBar_main_activity
return fragmentView
}
override fun onResume() {
super.onResume()
progressBar.visibility = View.VISIBLE
}
let say I have 2 fragments
I navigate from FragmentA to FragmentB. using the code below
val eventDetailDestination = UserControlFragmentDirections.actionGlobalDestinationEventDetail(selectedEvent)
Navigation.findNavController(fragmentView).navigate(eventDetailDestination)
in FragmentB, after the user do some actions in FragmentB, then they need to go back to FragmentA
here is the problem ....
if the user goes back from FragmentB to FragmentA using back button in the top left corner in action bar/toolbar, the progress bar in FragmentA will show up.
but if the user goes back using hardware back button in the bottom right, the progress bar in FragmentA will never show. even though I am sure progressBar.visibility = View.VISIBLE has been executed in FragmentA ?
I have tried to read the difference between back button in toolbar and hardware back button. but I have no Idea why this happened. please help :)
That happens because fragmentA is getting deleted and recreated from scratch when using the hardware back button which is the expected result. You can override the default behaviour like below:
In MainActivity
override fun onSupportNavigateUp(): Boolean {
//Use component backstack pop
return Navigation.findNavController(fragmentView).navigateUp()
}
Sheding some more light
Up vs. Back
The Up button is used to navigate within an app based on the
hierarchical relationships between screens. For instance, if screen A
displays a list of items, and selecting an item leads to screen B
(which presents that item in more detail), then screen B should offer
an Up button that returns to screen A.
If a screen is the topmost one in an app (that is, the app's home), it
should not present an Up button.
..
The system Back button is used to navigate, in reverse chronological
order, through the history of screens the user has recently worked
with. It is generally based on the temporal relationships between
screens, rather than the app's hierarchy.
When the previously viewed screen is also the hierarchical parent of
the current screen, pressing the Back button has the same result as
pressing an Up button—this is a common occurrence. However, unlike the
Up button, which ensures the user remains within your app, the Back
button can return the user to the Home screen, or even to a different
app.
Reference:
Android Navigation with Back and Up

Android Jetpack Navigation nested tab backward navigation strange behaviour

So I'm trying Jetpack navigation component with BottomNavigationView. I created two layer of BottomNavigationView, and the structure looks like this:
MainActivity (with nav_host_fragment, navigation_graph, bottom_navigation)
FragmentA
FragmentB
FragmentC (with nested_nav_host_fragment, nested_navigation_graph, nested_bottom_navigation)
FragmentCA
FragmentCB
FragmentCC
I have no problem navigating forward, but I couldn't navigate backward properly.
For example, when I navigation from A -> B -> C, and in C navigate CA -> CB -> CC, then clicking back button or calling navControler back, it should go from CC -> CB -> CA -> B -> A, but it straightly went to A instead.
The minimum demo project can be found here, hope someone can help, thanks.
By default, Fragments do not pop anything added to the back stack of child fragments.
To get the system back button to pop child Fragments of your Fragment C, you must specifically opt into that behavior by calling setPrimaryNavigationFragment().
This can be done anywhere in your Fragment after your Fragment is attached. For example, you can update your FragmentC to do it in onActivityCreated():
override fun onActivityCreated(savedInstanceState: Bundle?) {
super.onActivityCreated(savedInstanceState)
NavigationUI.setupWithNavController(nested_bottom_navigation,
activity?.findNavController(R.id.nested_nav_host_fragment)?:return)
// This routes the system back button to this Fragment
requireFragmentManager().beginTransaction()
.setPrimaryNavigationFragment(this)
.commit()
}
This is actually the same technique that the app:defaultNavHost="true" attribute on NavHostFragment is using under the hood.

Categories

Resources