Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can you handle dismissing a DialogFragment (compatibility lib) upon completion of an AsyncTask

There are numerous posts about how to handle a configuration change during an AsyncTask, but none I have found give a clear solution regarding apps that are in background (onPause()) when an AsyncTask finishes and tries to dismiss a DialogFragment (compatibility library).

Here is the problem, if I have an AsyncTask running that should dismiss a DialogFragment in onPostExecute(), I get an IllegalStateException if the app is in the background when it tries to dismiss the DialogFragment.

private static class SomeTask extends AsyncTask<Void, Void, Boolean> {

    public SomeTask(SomeActivity tActivity)
    {
        mActivity = tActivity;
    }

    private SomeActivity mActivity;

    /** Set the view during/after config change */
    public void setView(Activity tActivity) {
        mActivity tActivity;
    }

    @Override
    protected Boolean doInBackground(Void... tParams) {
        try {
          //simulate some time consuming process
          TimeUnit.SECONDS.sleep(3);
        } catch (InterruptedException ignore) {}
        return true;
    }

    @Override
    protected void onPostExecute(Boolean tRouteFound) {
        mActivity.dismissSomeDialog();  
    }

}

The Activity looks like this:

import android.support.v4.app.FragmentActivity;
import android.support.v4.app.FragmentManager;

public class SomeActivity extends FragmentActivity {

    public void someMethod() {
        ...
        displaySomeDialog();
        new SomeTask(this).execute();
        ...
    }

    public void displaySomeDialog() {
        DialogFragment someDialog = new SomeDialogFragment();
        someDialog.show(getFragmentManager(), "dialog");
    }

    public void dismissSomeDialog() {
        SomeDialogFragment someDialog = (SomeDialogFragment) getFragmentManager().findFragmentByTag("dialog");
        someDialog.dismiss();
    }

    ....

}

Works fine UNLESS the app switches to background while SomeTask is still running. In that case, when SomeTask tries to dismissSomeDialog(), I get an IllegalStateException.

05-25 16:36:02.237: E/AndroidRuntime(965): java.lang.IllegalStateException: Can not perform this action after onSaveInstanceState

All of the posts I've seen seem to point in some kludgy direction with elaborate workarounds. Isn't there some android way of handling this? If it were a Dialog instead of a DialogFragment, then the Activity's dismissDialog() would handle it correctly. If it were a real DialogFragment instead of one from the ACP, then dismissAllowingStateLoss() would handle it. Isn't there something like this for the ACP version of DialogFragment?

like image 685
Chuck Krutsinger Avatar asked May 25 '12 20:05

Chuck Krutsinger


People also ask

How do you dismiss a DialogFragment?

tl;dr: The correct way to close a DialogFragment is to use dismiss() directly on the DialogFragment. Control of the dialog (deciding when to show, hide, dismiss it) should be done through the API here, not with direct calls on the dialog.

How do I know if DialogFragment is showing?

Showing the DialogFragment Instead, use the show() method to display your dialog. You can pass a reference to a FragmentManager and a String to use as a FragmentTransaction tag.

What is DialogFragment in Android?

Android DialogFragments. DialogFragment is a utility class which extends the Fragment class. It is a part of the v4 support library and is used to display an overlay modal window within an activity that floats on top of the rest of the content. Essentially a DialogFragment displays a Dialog but inside a Fragment.


2 Answers

Fragments are saved as part of each Activity's state, so performing transactions after onSaveInstanceState() has been called technically doesn't make sense.

You definitely don't want to use commitAllowingStateLoss() to avoid the exception in this case. Consider this scenario as an example:

  1. The Activity executes an AsyncTask. The AsyncTask shows a DialogFragment in onPreExecute() and starts executing its task on a background thread.
  2. The user clicks "Home" and the Activity is stopped and forced into the background. The system decides that the device is pretty low on memory so it decides that it should also destroy the Activity too.
  3. The AsyncTask completes and onPostExecute() is called. Inside onPostExecute() you dismiss the DialogFragment using commitAllowingStateLoss() to avoid the exception.
  4. The user navigates back to the Activity. The FragmentManager will restore the state of its fragments based on the Activity's saved state. The saved state doesn't know about anything after onSaveInstanceState() has been called, so the request to dismiss the DialogFragment will not be remembered and the DialogFragment will be restored even though the AsyncTask has already completed.

Because of weird bugs like these that can occasionally happen, it's usually not a good idea to use commitAllowingStateLoss() to avoid this exception. Because the AsyncTask callback methods (which are called in response to a background thread finishing its work) have absolutely nothing to do with the Activity lifecycle methods (which are invoked by the system server process in response to system-wide external events, such as the device falling asleep, or memory running low), handling these situations require you to do a little extra work. Of course, these bugs are extremely rare, and protecting your app against them will often not be the difference between a 1 star rating and a 5 star rating on the play store... but it is still something to be aware of.

Hopefully that made at least some sense. Also, note that Dialogs also exist as part of the Activitys state, so although using a plain old Dialog might avoid the exception, you would essentially have the same problem (i.e. dismissing the Dialog wouldn't be remembered when the Activity's state is later restored).

To be frank, the best solution would be to avoid showing a dialog throughout the duration of the AsyncTask. A much more user-friendly solution would be to show a indeterminate progress spinner in the ActionBar (like the G+ and Gmail apps, for example). Causing major shifts in the user interface in response to asynchronous callbacks is bad for the user experience because it is unexpected and abruptly yanks the user out of what they are doing.

See this blog post on the subject for more information.

like image 188
Alex Lockwood Avatar answered Oct 01 '22 12:10

Alex Lockwood


To get around the illegal state exception issue and essentially implement a dismissAllowingStateLoss() can be done using the following.

getFragmentManager().beginTransaction().remove(someDialog).commitAllowingStateLoss();

This should solve the issue without the hacky code. The same can also be applied for show if you have threads communicating through a handler with the UI thread using dialog.show(); Which can cause an illegal state exception as well

getFragmentManager().beginTransaction().add(someDialog).commitAllowingStateLoss();


@joneswah is correct, given the posters question. If you are using the support library, replace
getFragmentManager()

with

getSupportFragmentManager()


For future Googlers: @Alex Lockwood raises good and valid concerns with this solution. The solution does solve the error and will work in most cases, but hints that there are issues with the approach in the original question, from a UX perspective.

The Activity should assume that the async task may not complete and that it will not perform onPostExecute(). Whatever UI action (ie, spinner, ideally not a dialog) is started to notify the user of the async operation, should have provisions to stop automatically either on a timeout or by tracking state and checking in onRestore/onResume type lifecycle events to ensure the UI is updated properly. Services may also be worth investigating.

like image 36
Zachary Moshansky Avatar answered Oct 01 '22 11:10

Zachary Moshansky