Here is another workaround to the problem. Like most workarounds, there are some downsides to this. I've outlined the downsides in "The downsides" 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 on 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 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.