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

4

17

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.

Misprize answered 23/4, 2021 at 21:33 Comment(0)
S
14

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:

  1. 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.

  2. 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.

Syncarpous answered 23/4, 2021 at 21:42 Comment(2)
Thank you very much for the explanation \o/. I'll try a workaround for that situation ^^Misprize
How does one go around this then? @SyncarpousHillie
C
2

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:

Caudad answered 2/8, 2021 at 21:39 Comment(0)
A
1

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()
        }
    }
    
}
Andersonandert answered 24/4, 2021 at 3:1 Comment(0)
O
1

@ianhanniballake made a good point why this is not entirely possible and why it works the way it works but there is actually a workaround. The workaround is not the most pleasant so keep this in mind. I've listed the drawbacks in it's own section below.

The theory

The workaround involves some "older" Android mechanisms, mechanisms that predate navigation controller (we didn't always have navigation controller). The workaround revolves around a few facts:

  1. All fragments live inside some FragmentManager. Navigation controller isn't magic, it still uses FragmentManagers under the hood. In fact you can think of Navigation controller as a wrapper around FragmentManager.
  2. All fragments come with it's own little FragmentManager. You may access it via childFragmentManager within the fragment. Any fragments launched in childFragmentManager are considered that fragment's children.
  3. When a fragment is moved to the "backstack", all of it's children move with it.
  4. When a fragment is restored, so are it's children.

With these four facts we can formulate a workaround.

The idea is if we show all DialogFragments on a fragment's childFragmentManager then we maintain the ability to navigate to other fragments without any dialog related issues. This is because when we navigate from say FragA to FragC, all of FragA's children is moved to the back stack. Since we launched the DialogFragment using childFragmentManager, the DialogFragment is automatically dismissed as well.

Now when the user moves back to our fragment (FragA), our DialogFragment is shown again because FragA's childFragmentManager is restored too. And our DialogFragment lives inside that childFragmentManager.

The implementation

Now that we know how we will workaround this issue, let's start implementing it.

For simplicity, let's reuse the example you have given. That is we will assume we have fragments FragA and FragC and dialog DialogB.

The first thing is that as nice as Navigation component is, if we want to do this, we cannot use it to launch our dialog. If you use safe args, you can continue to reap it's benefits though since technically safe args isn't tied to Navigation component. Here's an example of launching Dialog B:

// inside FragA
fun onLaunchBClick() {
  val parentFragment = parentFragment ?: return
  
  DialogB()
    .apply {
        // we can still use safe args
        arguments = DialogBArgs(myArg1, myArg2).toBundle()
    }
    .show(parentFragment?.childFragmentManager, "DialogB")
}

Now we can have DialogB launch FragC, but there's a catch. Because we are using childFragmentManager, navigation controller doesn't actually see DialogB. This means that to the navigation controller, we are launching FragC from FragA. This can create an issue here if there are multiple edges to DialogB in the nav graph. The workaround to this is to make all of DialogB's directions global. This is ultimately the downside to this workaround. In this case we can declare a global action to FragC and launch it via

// inside DialogB
fun onLaunchCClick() {
  val direction = NavMainDirections.actionGlobalFragC()
  findNavController().navigate(direction)
}

The downsides

So there are some obvious downsides to this approach. The biggest one is all fragments the dialog can navigate to should be declared as global actions. The only outlier being if the dialog has exactly 1 edge. If the dialog only has a single edge and it is unlikely a new edge will ever be added, you can technically just add actions to it's only parent fragment instead.

As an example if DialogC can launch FragmentC and FragmentD and DialogC can be launched from FragmentA and FragmentZ (2 edges) then DialogC must use global actions to launch FragmentC or FragmentD.

The other downside is we can no longer use Navigation controller for launching dialog fragments that need to launch other non-dialog fragments. This downside is milder since we can at least still use safe args.

The final downside is that performance might be slightly worse. Consider an example where we have fragment FragA launch DialogB launch FragC. Now if the user taps back, FragA will be restored. But since DialogB is FragA's child, DialogB will also be restored. This means that an extra fragment will need to be loaded and restored, reducing the performance of the back action. In practice this cost should be small as long as your fragment isn't saving a huge amount of state and as long as each fragment does not have too many children.

Overlie answered 24/3, 2023 at 1:55 Comment(1)
"Any fragments launched in childFragmentManager are considered that fragment's children." - Thanks for this line.Attu

© 2022 - 2024 — McMap. All rights reserved.