3

I have FragmentA, FragmentB and DialogFragment(BottomDialogFragment). I I abbreviated them as A,B and D

D will be shown after the button in A is clicked. It means A -> D

B will be shown after the button in D is clicked. It means D -> B

I config them in navigation.xml

<fragment
        android:id="@+id/A"
        android:name="com.example.A">

    <action
        android:id="@+id/A_D"
        app:destination="@id/D" />
</fragment>



<dialog
        android:id="@+id/D"
        android:name="com.example.D">

    <action
        android:id="@+id/D_B"
        app:destination="@id/B" />
</dialog>


<fragment
        android:id="@+id/B"
        android:name="com.example.B">
</fragment>

Now when I click the button in A, the fragment will jump to D.

Then I click the button in D, the fragment will jump to B.

But when I pop the navigation stack in B, it will back to A, and the D doesn't show.

What should I do? I want the D still exists on the surface of A.

Zain
  • 37,492
  • 7
  • 60
  • 84
pnkj
  • 406
  • 5
  • 17
  • So to clarify the behaviour you are looking for is: A —> D, D —> B. Then B —> D after that then what? Are you using the Jetpack navigation library or managing the Fragments manually? –  Jul 30 '21 at 06:55
  • In B, I call `controller.popBack()`, so it will back to D. But in fact, it will back to A, and my D will disappear..... I use Jetpack navigation – pnkj Jul 30 '21 at 07:18
  • A question, when you want the D (dialog) to be shown after back from B, what's the backgound of D you are expecting? (suppose your dialog is not fullscreen) – Sam Chen Jul 30 '21 at 15:10
  • Yeah, D is not fullscreen. It was show above A. So I expect D's background to be A after back from B. – pnkj Jul 31 '21 at 15:40

4 Answers4

3

What should I do? I want the D still exists on the surface of A.

So far, this is not possible, because dialogs are handled in a separate window than the activities/fragments; and therefore their back stack is handled differently And this is because Dialog implements the FloatingWindow interface.

Check this answer for more clarification.

But to answer your question, there are two approaches in mind:

  1. Approach 1: Change fragment B to a DailogFragment, and in this case both B & D are dialogs and therefore when you popup the stack to back from B to D, you'll still see D showing.
  2. Approach 2: To have a flag that is set if you return from B to D, and when if so, you re-show D.

Actually, approach 2 isn't that good, because it doesn't keep D in the back stack while you go from D to B; it's just a workaround; also the user would see the dialog transitioning/fade animation while it returns from B to D; so it's not natural at all. So, here only approach 1 will be discussed.

Approach 1 in detail:

Pros:

  • It's very natural and will keep the back stack the same as you would like to.

Cons:

  • DialogFragment B has a limited window than the normal fragment/activity.
  • B is no longer a normal fragment, but a DialogFragment, so you might encounter some other limitations.

To solve the limited window of B, you can 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 apply it to B using getTheme():

class FragmentB : DialogFragment() {

    override fun onCreateView(
        inflater: LayoutInflater,
        container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View? {
        return layoutInflater.inflate(R.layout.fragment_b, container, false)
    }

    override fun getTheme(): Int = R.style.DialogTheme
    
}

Also you need to change B in the navigation graph to a dialog:

<dialog
        android:id="@+id/B"
        android:name="com.example.B">
</dialog>

Preview:

Zain
  • 37,492
  • 7
  • 60
  • 84
  • 2
    this is a very clean, and complete answer ! thank you. I have a different approach. Check my answer below. – A.David Aug 03 '21 at 05:16
1

Refer to this link for a full working example.

You need to leverage the NavigationUI global action (as explained here) in order to be able to navigate "back" to a destination. Put this code into the main_graph xml:

<action android:id="@+id/action_global_fragmentD" app:destination="@id/fragmentD"/>

Next, in your activity add these to catch back press:

class MainActivity: AppCompatActivity {
...
    var backPressedListener: OnBackPressedListener? = null
    override fun onBackPressed() {
        super.onBackPressed()
        backPressedListener?.backHaveBeenPressed()
    }
}
interface OnBackPressedListener {
   fun backHaveBeenPressed()
}

this is like delegates in swift

Next in FragmentB, add these:

class FragmentB: Fragment(), OnBackPressedListener {
     override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
     ): View? {
        // Inflate the layout for this fragment
        (activity as MainActivity).backPressedListener = this
        return inflater.inflate(R.layout.fragment_b, container, false)
    }

    override fun backHaveBeenPressed() {
        // show Dialog
        findNavController().navigate(R.id.action_global_fragmentD)
    }   
 }

You can then navigate back to the DialogFragment as you require. This approach does not use the popBackStack because your use case is a custom behavior not handled by the NavigationUI framework (you need to implement it).

A.David
  • 172
  • 9
0

There is no need to pop the stack with controller.popBack() as the Stack is managed by the Navigation Library. Note that the stack operates on a LIFO basis so that is why the Fragment disappeared.

You need to add more navigation graph actions:

<?xml version="1.0" encoding="utf-8"?>
 <navigation xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:id="@+id/navigation"
    app:startDestination="@id/A">

    <fragment
        android:id="@+id/A"
        android:name="com.example.A"
        android:label="A" >
        <action
            android:id="@+id/action_A_to_D"
            app:destination="@id/D" />
    </fragment>

    <dialog
        android:id="@+id/D"
        android:name="com.example.D"
        android:label="D" >
        <action
            android:id="@+id/action_D_to_B"
            app:destination="@id/B" />
        <action
            android:id="@+id/action_D_to_A"
            app:destination="@id/A" />
    </dialog>

    <fragment
        android:id="@+id/B"
        android:name="com.example.B"
        android:label="B" >
        <action
            android:id="@+id/action_B_to_D"
            app:destination="@id/D" />
    </fragment>
</navigation>

Then in your dialog Fragment add the following:

For OK/Yes:--


    private fun doNav() {
        NavHostFragment.findNavController(this).navigate(R.id.action_fragmentD_to_fragmentB)
    }

For CANCEL/No:--


    private fun doBackNav() {
        NavHostFragment.findNavController(this).navigate(R.id.action_fragmentD_to_fragmentA)
    }

Finally in the B Fragment, override the back press button and execute this:

        Navigation.findNavController(requireView()).navigate(R.id.action_fragmentB_to_fragmentD)

  • This approach will not work. It will cause my D to be shown directly on top of the B – pnkj Jul 31 '21 at 15:55
  • Explain your use case in more detail and provide some diagrams –  Aug 01 '21 at 03:43
  • 1
    as explained in this thread https://stackoverflow.com/questions/50311637/navigation-architecture-component-dialog-fragments you can not put dialog fragment in Navigation UI framework. – A.David Aug 02 '21 at 12:47
0

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:

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

idunnololz
  • 8,058
  • 5
  • 30
  • 46