问题
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:
- An activity executes an AsyncTask.
- The user presses the "Home" key, causing the activity's onSaveInstanceState() and onStop() methods to be called.
- The AsyncTask completes and onPostExecute() is called, unaware that the Activity has since been stopped.
- 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:
- 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);
}
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.
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
回答1:
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 AsyncTask
s and Volley Request
s should only be performed from Service
s and not Activity
s, because Service
s continue to run even if the user "exits" the app.
回答2:
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;
}
来源:https://stackoverflow.com/questions/28683276/how-to-deliver-and-persist-changes-to-the-ui-from-an-asynchronous-task-hosted-by