13

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?

Alex Lockwood
  • 83,063
  • 39
  • 206
  • 250
Chuck Krutsinger
  • 2,830
  • 4
  • 28
  • 50
  • 1
    Take a look at my [**blog post**](http://www.androiddesignpatterns.com/2013/08/fragment-transaction-commit-state-loss.html) on this subject... maybe it will help. – Alex Lockwood Aug 21 '13 at 15:11

6 Answers6

23

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.

Alex Lockwood
  • 83,063
  • 39
  • 206
  • 250
  • Alex, could you clarify as to how one would implement such a solution to track the state of the Async task while in the background in order to avoid "commitAllowingStateLoss()"? You are definitely correct about the UI impact and the potential for a discontinuous UI experience. – Zachary Moshansky Mar 14 '14 at 20:54
  • @Alex, Really been reading a lot of your stuff for a while now. I'm having almost the same problem but haven't been able to implement any of your solutions yet. Please help me take a look at [this](http://stackoverflow.com/questions/22465289/cannot-perform-this-task-after-onsavedinstancestate-while-dismissing-dialog-frag), Thanks. – bibi_bryan Mar 17 '14 at 21:05
  • Facing the same problem when pressing home button after the rotation, any ideas to implement an independent way, without tracking the activity state? – X-HuMan May 19 '16 at 11:15
17

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.

Zachary Moshansky
  • 1,673
  • 18
  • 32
  • Glad it helped. I faced the same issue recently and put a ridiculous amount of effort into tracking down a solution. The only other one I found was basically change the compatibility library source, or extend the class and implement the method yourself. – Zachary Moshansky Nov 27 '12 at 18:09
  • 1
    This should use getSupportFragmentManager() rather than getFragmentManager() – joneswah Sep 22 '13 at 12:53
  • Maybe this is an old thread, but my question is part of this and still actual. How to deal with the point when we rotate and then press home button? The dialog is not dismissing at all without an exception. Without rotation dismissing after home button is fine. – X-HuMan May 19 '16 at 11:12
3

You should cancel your AsyncTask in onPause() if the onPostExecute() is going to update the UI. You shouldn't try to update the UI while your activity has been paused.

Eg. in your onPause():

if (task != null) task.cancel(true);

If you want the changes from the task to persist to the next time, then store the data/changes in doInBackground() and then update the UI when your activity/fragment/dialog gets resumed.

If you don't want the changes from the task to persist, then don't store the changes until onPostExecute()

Zambotron
  • 699
  • 5
  • 14
  • I feel like Android OS is making me turn this into spaghetti where the Activity has to keep track of if the task is still running when its state changes and the task has to be aware of whether the Activity is visible to decide what comes next. Not good. I would really like to avoid this. Surely there is a more straightforward way to tackle this. And surely I'm not the first developer to encounter the issue. Someone must have a clean idiom for handling the scenario of a task running when the Activity goes into the background. – Chuck Krutsinger May 30 '12 at 22:55
  • To elaborate on the above comment, clearly the framework can handle Activity.dismissDialog() correctly even when in background. DialogFragment.dismissAllowingStateLoss() works correctly even when in background. However, in ACP there is no DialogFragment.dismissAllowingStateLoss(). Still, the Android folks must have encountered this situation before and must have a suggested way to handle it. – Chuck Krutsinger May 30 '12 at 23:25
  • 1
    Just extend DialogFragment and make dismissAllowStateLoss(), or override dismiss which is what I do. Use getFragmentTransaction(), then remove(this), then commitAllowStateLoss(). Works fine. – Noah Seidman Jun 27 '12 at 20:11
0

When Android stops your app because the user hit the back or home button, your dialogs are closed for you. Usually the trick is to preserve the dialogs between onStop()/onStart(). So unless you need to do more than just close the dialog, I'd say don't worry about it.

EDIT: On your activity that hosts the dialog, you may still want to close the dialog if it's still open inside onStop(). This helps prevent memory leaks. But this doesn't need to be triggered from AsyncTask.

Like i said above, the problem is what happens when you hit onStart() again and your AsyncTask is NOT finished yet. You'll need to figure out a way to determine that and re-open that dialog if needed.

Glaucus
  • 848
  • 8
  • 14
  • I have a working approach for onStop() and onStart(). Where the problem comes in is with onPause(), ie. my Activity is no longer visible. In the case of a pause, the dialog remains open and the AsyncTask continues executing. Once the task completes, it attempts to close a ProgressDialog using Activity.dismissDialog(), which works fine even while paused. Then it tries to dismiss a DialogFragment (ACP version), by calling DialogFragment.dismiss(). This results in an InvalidStateException. – Chuck Krutsinger May 30 '12 at 16:39
  • I'm afraid you may need to rethink your design. The simplest way to do what you want with the leat work may be to just close the dialog in the onPause(). You could also try to cancel the AsyncTask in the onPause, but that may depend on what the AsyncTask is doing. the AsyncTask class does have a cancel method, but that doesn't actually kill the spawned thread, it simply sets a flag and your code inside the AsyncTask must somehow deal with it. Some IO tasks, such as network traffic, can be particularly tricky to cancel or abort. In which case, trying to cancel the AsyncTask could be a problem. – Glaucus Jun 04 '12 at 03:38
  • I would also suggest you consider a service. The service could then broadcast the results to your activity. With this model no attempts to access the dialog would be made outside of your activity. To be honest, I sometimes wish they never made AsyncTask for reasons such as this, it actually makes dealing with the edge cases harder. – Glaucus Jun 04 '12 at 03:42
  • Glaucus maybe you should move your comments into your answer. @ChuckKrutsinger - perhaps once Glaucus has done that and you think that his advice is correct then you should mark his answer as accepted? – Kev Jun 05 '12 at 23:03
  • I'm still evaluating answers and haven't settled on any yet. Right now I'm thinking that my question is the wrong question. In other words, the question shouldn't be how do I handle the dismiss while not visible, but rather should I allow my AsyncTask to continue while the Activity is paused, to which I'm starting to understand the answer is no. I'm reworking the design to pause or even cancel the AsyncTask when the activity gets paused, which I will probably handle in onSaveInstanceState(). – Chuck Krutsinger Jun 06 '12 at 15:08
0

After numerous redesigns I finally settled on an approach that seems to work. However, I haven't been able to find certain aspects of my design documented elsewhere in discussions of Android development. So I'd be interested in any feedback.

In summary, what I had to do is this:

  • onSaveInstanceState() - if SomeTask is running, I use a simple lock to block SomeTask from exiting doInBackground() during pause.
  • onResume() - I had to put in a switch statement to handle different resume situations. If launching app I do nothing as nothing is paused, if restarting after being hidden or after config change I release the lock so that the preserved SomeTask instance can resume where it left off, etc.
  • onDestroy() - I cancel SomeTask if it is running.

I'll put the code fragments for this solution in my original post.

Chuck Krutsinger
  • 2,830
  • 4
  • 28
  • 50
0

getActivity().finish(); in the DialogFragment worked for me.

jediz
  • 4,459
  • 5
  • 36
  • 41
Eli
  • 1
  • 1