I appreciate the numerous postings regarding AsyncTask on a rotation change. I have the following problem when using the compatability lib and trying to dismiss a DialogFragment
in onPostExecute
.
I have a fragment which fires of an AsyncTask which displays a progress DialogFragment
, then in onPostExecute
dismisses the dialog and then potentially throws up another DialogFragment
.
If when the progress dialog is being displayed I put the application into the background I get the following for my fragment:
1) onPause
2) onSaveInstanceState
3) onPostExecute
in which I try to dismiss and invoke a dialog.
I get an IllegalStateException
because I'm trying to effectively commit a transaction when the activity has saved its state and I understand this.
On a rotation I've assumed (perhaps incorrectly) that I wouldn't get an onPostExecute
until the activity has been recreated. However, when putting the application into the background I assumed (definitely incorrectly) that the onPostExectute
wouldn't get called while the fragment/activity was paused.
My question is, is my solution to simply detect in onPostExecute
that the fragment/activity is paused and simply perform what I need to do in onResume
instead? Seems somewhat ugly to me.
Thanks in advance, peter.
Edit 1
Need to support 2.1 and above
Edit 2
I have considered showing the dialog using FragmentTransaction:add
and FragmentTransaction:commitAllowingStateLoss
however this isn't without its problems.
If you need to synchronize your task with the activity lifecycle, I believe that Loaders are exactly what you need. More specifically, you should use AsyncTaskLoader to do the job. So now instead of running an AsyncTask, you launch your loader, then wait for response in a listener. If the activity is paused, you won't get a callback, this part will be managed for you.
There is another way to handle this task: using a fragment which retains its instance. The general idea is that you create a fragment without UI and call setRetainInstance(true)
. It has a task which is being notified about the activity being available or not. If not, the task's thread suspends until an activity becomes available.
Another way of achieving what you require is to implement the PauseHandler class that I documented in this post.
Then in your onPostExecute method call sendMessage() to post your message into the handler.
When your application resumes the action will be handled.
Rather then using BroadcastReceiver, I prefer using bus libraries like guava, otto or eventbus. Their performance is much better then the broadcast receiver implementation.
I came up with a solution for this problem without any major workaround:
The basic idea how to maintain a progressdialog and a asynctask is described in this blogentry (of course I used the AsyncTaskComplex-Version). All credits go to the author of this blogentry, I only added a tiny thing:
Obviously I'm not using showDialog() anymore. Instead I stick with DialogFragments.
The second tweak is the importent one and also solves the problem with the IllegalStateException:
Instead of only telling the asynctask in onRetainCustomNonConfigurationInstance() that there is no more activity I also do it in onPause(). And instead of only telling the asynctask in onCreate() that there is a new activity I also do it in onResume().
And there you go, your AsyncTask will not try to inform your activity about his finish causing an IllegalStateException when the activity is not visible.
If you would like to see more code instead of words, leave a comment.
/edit:
Sourcecode to show my solution, which I think is a pretty decent one :)
public class MyActivity extends Activity {
private MyTask mTask;
@Override
protected void onCreate(Bundle pSavedInstanceState) {
super.onCreate(pSavedInstanceState);
setContentView(R.layout.editaccount);
Object retained = getLastCustomNonConfigurationInstance();
if ( retained instanceof NewContactFolderIdTask ) {
mTask = (MyTask) retained;
mTask.setActivity(this);
}
}
@Override
protected void onPause() {
if(mTask != null) {
mTask.setActivity(null);
}
super.onPause();
}
@Override
public Object onRetainCustomNonConfigurationInstance() {
if(mTask != null) {
mTask.setActivity(null);
return mTask;
}
return null;
}
@Override
protected void onResume() {
if(mTask != null) {
mTask.setActivity(this);
}
loadValues(); // or refreshListView or whatever you need to do
super.onResume();
}
public void onTaskCompleted() {
loadValues(); // or refreshListView or whatever you need to do
DialogFragment dialogFragment = (DialogFragment) getSupportFragmentManager().findFragmentByTag(PROGRESS_DIALOG_FRAGMENT);
if(dialogFragment != null) {
dialogFragment.dismiss();
}
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
MenuInflater menuInflater = getMenuInflater();
menuInflater.inflate(R.menu.main, menu);
return super.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
// app icon in Action Bar clicked; go home
Intent intent = new Intent(this, OXClient.class);
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
startActivity(intent);
return true;
case R.id.menu_refresh:
mTask = new MyTask(this);
mTask.execute();
break;
}
return super.onOptionsItemSelected(item);
}
private class NewContactFolderIdTask extends AsyncTask<Boolean, Integer, Bundle> {
private MyActivity mActivity;
private boolean mCompleted;
private NewContactFolderIdTask(MyActivity pActivity) {
this.mActivity = pActivity;
}
public void setActivity(MyActivity pActivity) {
this.mActivity = pActivity;
if(mCompleted) {
notifiyActivityTaskCompleted();
}
}
private void notifiyActivityTaskCompleted() {
if(mActivity != null) {
mActivity.onTaskCompleted();
}
}
@Override
protected Bundle doInBackground(Boolean... pBoolean) {
// Do your stuff, return result
}
@Override
protected void onPreExecute() {
DialogFragment newFragment = ProgressDialogFragment.newInstance();
newFragment.show(getSupportFragmentManager(), PROGRESS_DIALOG_FRAGMENT);
}
@Override
protected void onPostExecute(Bundle pResult) {
mCompleted = true;
notifiyActivityTaskCompleted();
}
}
}
On How to handle Handler messages when activity/fragment is paused I offer another approach using a BroadcastReceiver.
I consider it cleaner more elegant and it offers the advantages that you can invoke code on your base fragment from everywhere within your app and by using sticky broadcasts your invocation can be "remembered" and executed after your fragment resumes.