0

I am using Navigation component to launch & dismiss an ErrorDialog. I noticed that when I try to re-open the error dialog on a re-try flow I get an error that the action can't be found. It works the first time the dialog is launched.

 findNavController().navigate(R.id.action_secondFragment_to_dialog_navigation)

I also noticed that I have to press the back button twice in order to return to the original page. I figured the issue was that the DialogFragment is not being removed as the current destination...and I was right.

When I use the debug inspector the second time you want to launch it shows that the current navigation destination is the dialog fragment.

Using a Handler & postDelayed with a small delay the issue is resolved. Just using a post on the main thread doesn't work; you need a small delay.

However, this doesn't seem like the right solution. Is this really a race condition or am I doing something wrong?

Here is the relevant code.

Here is where I dispatch the DialogFragment from second fragment

    private fun showErrorDialog() {
        if (!isRemoving && isVisible) {
            findNavController().navigate(R.id.action_secondFragment_to_dialog_navigation)
        }
    }

Here is where I set the result and exit the dialog (In ErrorDialogFragment)

    private fun navigateBackWithResult(result: Boolean) {
        findNavController()
            .previousBackStackEntry
            ?.savedStateHandle
            ?.set(DIALOG_RESULT, result)
    }

Here is where I am listening for the result from the ErrorDialog


    private fun setupErrorDialogListener() {
        findNavController()
            .currentBackStackEntry
            ?.savedStateHandle
            ?.getLiveData<Boolean>(ErrorDialogFragment.DIALOG_RESULT)
            ?.observe(viewLifecycleOwner) {
                Log.d("dialog returned in observer with $it", TAG)
                it?.let {
                    //Solves an issue with navigation component after retrying dialog.  My guess
                    //is that you need to wait for error dialog fragment transaction to complete and be removed from backstack
                    //Does not work without the small delay.
                    Handler(Looper.getMainLooper()).postDelayed(
                        {
                            if (it) binding.vm?.refreshCalled() else Log.d("user declined to refresh", TAG)
                        },
                        50,
                    )
                }.whenNull { Log.e("failed to get user response from fragment through navigator", TAG) }
            }
    }

Here is the navigation XML for the fragment in which i launch the dialog

    <fragment
        android:id="@+id/secondFragment"
        android:name="ca.xyz.android.fragments.secondFragment"
        android:label="@string/second_fragment"
        tools:layout="@layout/second_fragment" />
        <action
            android:id="@+id/action_secondFragment_to_dialog_navigation"
            app:destination="@id/dialog_navigation"
            app:launchSingleTop="false"
            app:popUpTo="@id/secondFragment">
            <argument
                android:name="DIALOG_RESULT"
                app:argType="boolean" />
        </action>
    </fragment>

(I don't think singleTop flag is doing anything, I will remove it and see what happens, but that isn't relevant.)

Here is the relevant navigation xml for the dialogfragment

<navigation android:id="@+id/dialog_navigation"
        app:startDestination="@id/error_dialog">
        <dialog
            android:id="@+id/error_dialog"
            android:label="@string/step_two_title"
            android:name="ca.xyz.android.fragments.ErrorDialogFragment">
            <argument
                android:name="DIALOG_RESULT"
                app:argType="boolean" />
        </dialog>
    </navigation>
user1743524
  • 655
  • 1
  • 7
  • 14
  • Did you get a chance to read the [Considerations when using dialog destinations and returning a result documentation](https://developer.android.com/guide/navigation/use-graph/programmatic#additional_considerations)? It states: "This also means that any Observer you set on the result LiveData will be triggered even while the dialog destinations is still on the screen." which sounds like exactly what you are experiencing? – ianhanniballake Aug 19 '23 at 21:24

1 Answers1

0

When you use this kind of callback (a.k.a currentBackStackEntry and previousBackStackEntry and as a live data), if the dialog is open, in the previous fragment the viewLifecycleOwner is still alive. That means, when you set the result, it is transferred synchronously, which means the error dialog remains visible. The action action_secondFragment_to_dialog_navigation must be called from secondFragment while it is in foreground, a.k.a when it is the top fragment. If it is considered inactive, this exception will be thrown.

The Handler().postDelayed() approach works because you give time for the error dialog to disappear to your second fragment, which becomes the top fragment due to the navigation change, allowing it to trigger the error fragment again. A simple post does not work because it can take more than one frame for the fragment to disappear completely (considering animations and such.), or for it to be registered in the navigator.

This can be considered as a race condition (but not exactly since it's not two threads, but two separate fragments / dialogs), you're trying to call the dialog again before it disappears.

You can either:

  • Move the dialog to the same graph and use its ID (a.k.a navigate(R.id.error_dialog)) which will not use the action and resolve the crash (this might create a small memory leak, be careful),
  • Wait for the dialog to disappear, or, your fragment to be the top fragment inside the navigator, by adding a temporary navigator.addOnDestinationChangedListener and detect when it is added, after that, call the dialog again.
Furkan Yurdakul
  • 2,801
  • 1
  • 15
  • 37
  • thank you for the detailed answer! I think the onDestinationChangeListener is the better solution but I'm a bit unclear about the implementation. The user requests the dialog to be reopend with the retry button. Should I attach the listener after the user clicks retry and then dispatch the dialog again when the listener returns with the secondFragment as the current destination? – user1743524 Aug 21 '23 at 16:18