2

Using a retained fragment to host asynchronous tasks is not a new idea (see Alex Lockwood's excellent blog post on the topic)

But after using this I've come up against issues when delivering content back to my activity from the AsyncTask callbacks. Specifically, I found that trying to dismiss a dialog could result in an IllegalStateException. Again, an explanation for this can be found in another blog post by Alex Lockwood. Specifically, this section explains what is going on:

Avoid performing transactions inside asynchronous callback methods.

This includes commonly used methods such as AsyncTask#onPostExecute() and LoaderManager.LoaderCallbacks#onLoadFinished(). The problem with performing transactions in these methods is that they have no knowledge of the current state of the Activity lifecycle when they are called. For example, consider the following sequence of events:

  1. An activity executes an AsyncTask.
  2. The user presses the "Home" key, causing the activity's onSaveInstanceState() and onStop() methods to be called.
  3. The AsyncTask completes and onPostExecute() is called, unaware that the Activity has since been stopped.
  4. A FragmentTransaction is committed inside the onPostExecute() method, causing an exception to be thrown.

However, it seems to me that this is part of a wider problem, it just happens that the fragment manager throws an exception to make you aware of it. In general, any change you make to the UI after onSaveInstanceState() will be lost. So the advice

Avoid performing transactions inside asynchronous callback methods.

Actually should be:

Avoid performing UI updates inside asynchronous callback methods.

Questions:

  1. If using this pattern, should you therefore cancel your task, preventing callbacks in onSaveInstanceState() if not rotating?

Like so:

@Override
public void onSaveInstanceState(Bundle outState)
{
    if (!isChangingConfigurations())
    {
        //if we aren't rotating, we need to lose interest in the ongoing task and cancel it
        mRetainedFragment.cancelOnGoingTask();
    }
    super.onSaveInstanceState(outState);
}
  1. Should you even bother using retained fragments at all for retaining ongoing tasks? Will it be more effective to always mark something in your model about an ongoing request? Or do something like RoboSpice where you can re-connect to an ongoing task if it is pending. To get a similar behaviour to the retained fragment, you'd have to cancel a task if you were stopping for reasons other than a config change.

  2. Continuing from the first question: Even during a config change, you should not be making any UI updates after onSaveInstanceState() so should you actually do something like this:

Rough code:

@Override
public void onSaveInstanceState(Bundle outState)
{
    if (!isChangingConfigurations())
    {
        //if we aren't rotating, we need to lose interest in the ongoing task and cancel it
        mRetainedFragment.cancelOnGoingTask();
    }
    else
    {
        mRetainedFragment.beginCachingAsyncResponses();
    }
    super.onSaveInstanceState(outState);
}

@Override
public void onRestoreInstanceState(Bundle inState)
{
    super.onRestoreInstanceState(inState);
    if (inState != null)
    {
        mRetainedFragment.stopCachingAndDeliverAsyncResponses();
    }
}

The beginCachingAsyncResponses() would do something like the PauseHandler seen here

Community
  • 1
  • 1
Sam
  • 3,453
  • 1
  • 33
  • 57
  • 1
    What do you mean by "Alex Lockwood's PauseHandler"? I'm not sure where I talked about that term. :) – Alex Lockwood Feb 23 '15 at 21:05
  • 1
    Apologies, I apparently started mentally assigning everything smart i read on the internet to you. Updated to reference the SO post I saw it in. – Sam Feb 23 '15 at 21:06
  • What type of async tasks are you performing (i.e. how long do they take, how frequently are they happening, what types of fragment transactions are you performing when they finish, etc?) – Alex Lockwood Feb 23 '15 at 21:14
  • I'm looking for general approaches really but let's go with tasks in the order of 1-10 seconds and not too frequently. Mostly network IO type things. Fragment transaction would be dismiss dialog. – Sam Feb 23 '15 at 21:17
  • I might be missing something here but wouldn't a `null` check on the `dialog` in the `onPostExecute` of the AsyncTask resolve this issue? – Marcus Feb 23 '15 at 21:20
  • Aren't AsyncTasks the preferred way to update the UI to avoid doing too much in the Main UI? – Kristy Welsh Feb 23 '15 at 21:25
  • Sorry I don't understand the phrase "preferred way to update the UI to avoid doing too much in the Main UI?" – Sam Feb 23 '15 at 21:28
  • Updating the UI from async callback methods like `onPostExecute()` isn't forbidden. You just have to take into the account the possibility that state loss might occur. If all you need to do is update a `TextView`'s text, you could update the text in the async callback method and that would be that. If this happens after `onSaveInstanceState()` is called, then the text change will not be remembered when the user navigates back... so you will need to account for this on your own (i.e. make a note that the async task completed in persistent storage and restore the change when the user comes back) – Alex Lockwood Feb 23 '15 at 21:36
  • This sounds like inventing your own saveinstancestate on top of the framework's existing mechanism? – Sam Feb 23 '15 at 21:38
  • Kind of. Android is designed around the idea that background apps can be abruptly killed at any time. If the user switches between apps causing your app to be stopped, the framework will call `onSaveInstanceState()` and record its current state in case it is later killed due to low memory. Since apps may be killed abruptly, the framework does not (and cannot) wait for background threads to finish before saving the application's state... so anything that is triggered by a background thread after the app's state has been saved needs to be manually accounted for by the developer. – Alex Lockwood Feb 23 '15 at 21:47
  • Or, alternatively, a background thread should be prevented from triggering between onRetainInstanceState and onRestoreInstanceState ? Hence robospice requires a call during `onStop()` https://github.com/stephanenicolas/robospice/wiki/Starter-Guide#spice-your-activity-or-fragment Also, the PauseHandler solution i suggest does something similar. – Sam Feb 23 '15 at 21:56
  • One possible solution I can think of is to separate the model from the view. Your background thread can save state information to the model (i.e. shared prefs or a SQLiteDatabase) and the view can either query the model or somehow receive direct notifications when the model changes (i.e. using a OnSharedPreferenceChangeListener or something similar). This will give you more control over when the UI is updated (i.e. the view can wait to be notified instead of changing immediately after a background task completes). Have you tried something like this? – Alex Lockwood Feb 24 '15 at 15:59
  • hi Alex I've literally just created a singleton called "TipsManager" in my application which has two states "fetching" or "idle" and is Observable. This sounds equivalent to what you are suggesting I think? All you need to store in the model to solve this particular problem is the "Am i doing this task" boolean, hence the two states. Essentially you let the activity drive it's display from its normal lifecycle by querying and observing the TipsManager. It must unobserve in onStop(). – Sam Feb 24 '15 at 16:21
  • By the way is it certainly true that state loss of the kind we are talking about is possible during a config change? Obviously, you've already covered that activity destruction/creation is done serially on the main thread, but perhaps that extends to save/restoreInstanceState ? – Sam Feb 24 '15 at 16:33
  • I answered my own question by scanning the source: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.0.2_r1/android/app/ActivityThread.java#3886 looking at this, it looks like onsaveinstancestate is done sequentially with `handleDestroyActivity` ... And so it would be impossible to update the UI an have it lost during a config change. – Sam Feb 24 '15 at 18:16

2 Answers2

0

From a developer's point of view, avoiding NPEs' in a live app is the first order of business. To methods like onPostExecute() of AsyncTask and onResume() & onError() in a Volley Request, add:

Activity = getActivity();
if(activity != null && if(isAdded())){

    // proceed ...

}

Inside an Activity it should be

if(this != null){

    // proceed ...

}

This is inelegant. And inefficient, because the work on other thread continues unabated. But this will let the app dodge NPEs'. Besides this, there is the calling of various cancel() methods in onPause(), onStop() and onDestroy().

Now coming to the more general problem of configuration changes and app exits. I've read that AsyncTasks and Volley Requests should only be performed from Services and not Activitys, because Services continue to run even if the user "exits" the app.

Yash Sampat
  • 30,051
  • 12
  • 94
  • 120
  • Out of interest, could you provide a reference to where you've read about Services and AsyncTasks? – Marcus Feb 23 '15 at 21:22
  • Sorry, this doesn't answer my question, my problem isn't with NPE's, it's with the approach to updating UI after `onSaveInstanceState` has been called. – Sam Feb 23 '15 at 21:26
  • 1
    @Marcus: I believe it was [this book](http://www.amazon.com/Efficient-Android-Threading-Asynchronous-Applications/dp/1449364136) – Yash Sampat Feb 23 '15 at 21:31
  • @ZygoteInit Thanks, appreciated. +1 for Swedish author :-) – Marcus Feb 23 '15 at 21:34
0

So I ended up digging around a bit on this myself and came up with quite a nice answer.

Although not documented to do so, activity state changes are performed in synchronous blocks. That is, once a config change starts, the UI thread will be busy all the way from onPause to onResume. Therefore it's unnecessary to have anything like beginCachingAsyncResponses as I had in my question as it would be impossible to jump onto the main thread after a config change started.

You can see this is true by scanning the source: http://grepcode.com/file/repository.grepcode.com/java/ext/com.google.android/android/5.0.2_r1/android/app/ActivityThread.java#3886 looking at this, it looks like onSaveInstancestate is done sequentially with handleDestroyActivity ... And so it would be impossible to update the UI an have it lost during a config change.

So this should be sufficient:

@Override
public void onSaveInstanceState(Bundle outState)
{
    if (!isChangingConfigurations())
    {
        //if we aren't rotating, we need to lose interest in the ongoing task and cancel it
        mRetainedFragment.cancelOnGoingTask();
    }
    super.onSaveInstanceState(outState);
}

From the retained fragment it's crucial to access the activity from the main thread:

public void onSomeAsyncNetworkIOResult(Result r)
{
    Handler mainHandler = new Handler(Looper.getMainLooper());
    Runnable myRunnable = new Runnable()
    {
        //If we were to call getActivity here, it might be destroyed by the time we hit the main thread
        @Override 
        public void run()
        {
            //Now we are running on the UI thread, we cannot be part-way through a config change
            // It's crucial to call getActivity from the main thread as it might change otherwise
            ((MyActivity)getActivity()).handleResultInTheUI(r);
        }
    };
    mainHandler.post(myRunnable);
    return;

}
Sam
  • 3,453
  • 1
  • 33
  • 57