@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:
- All fragments live inside some
FragmentManager
. Navigation controller isn't magic, it still uses FragmentManager
s under the hood. In fact you can think of Navigation controller as a wrapper around FragmentManager
.
- 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.
- When a fragment is moved to the "backstack", all of it's children move with it.
- 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 DialogFragment
s 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.