如何在屏幕旋转期间处理异步任务?

我读了很多关于如何保存我的实例状态或如何处理我的活动在屏幕旋转期间被破坏。

似乎有很多种可能性,但我还没有找出哪种方法最适合检索 AsyncTask 的结果。

我有一些 AsyncTask,它们只是重新启动并调用活动的 isFinishing()方法,如果活动完成了,它们不会更新任何东西。

问题是,我有一个 Task,它对 Web 服务执行一个请求,这个请求可能失败或成功,重新启动任务将导致用户的经济损失。

你将如何解决这个问题? 可能的解决方案有哪些优点或缺点?

47125 次浏览

My first suggestion would be to make sure you actually need your activity to be reset on a screen rotation (the default behavior). Every time I've had issues with rotation I've added this attribute to my <activity> tag in the AndroidManifest.xml, and been just fine.

android:configChanges="keyboardHidden|orientation"

It looks weird, but what it does it hand off to your onConfigurationChanged() method, if you don't supply one it just does nothing other than re-measure the layout, which seems to be a perfectly adequate way of handling the rotate most of the time.

This is the most interesting question I've seen regarding to Android!!! Actually I've been already looking for the solution during the last months. Still haven't solved.

Be careful, simply overriding the

android:configChanges="keyboardHidden|orientation"

stuff is not enough.

Consider the case when user receives a phone call while your AsyncTask is running. Your request is already being processed by server, so the AsyncTask is awaiting for response. In this moment your app goes in background, because the Phone app has just come in foreground. OS may kill your activity since it's in the background.

You can check out how I handle AsyncTasks and orientation changes at code.google.com/p/shelves. There are various ways to do it, the one I chose in this app is to cancel any currently running task, save its state and start a new one with the saved state when the new Activity is created. It's easy to do, it works well and as a bonus it takes care of stopping your tasks when the user leaves the app.

You can also use onRetainNonConfigurationInstance() to pass your AsyncTask to the new Activity (be careful about not leaking the previous Activity this way though.)

Why don't you always keep a reference to the current AsyncTask on the Singleton provided by Android?

Whenever a task starts, on PreExecute or on the builder, you define:

((Application) getApplication()).setCurrentTask(asyncTask);

Whenever it finishes you set it to null.

That way you always have a reference which allows you to do something like, onCreate or onResume as appropriated for your specific logic:

this.asyncTaskReference = ((Application) getApplication()).getCurrentTask();

If it's null you know that currently there is none running!

:-)

In Pro android 4. author has suggest a nice way, that you should use weak reference.

Weak reference note

To my point of view, it's better to store asynctask via onRetainNonConfigurationInstance decoupling it from the current Activity object and binding it to a new Activity object after the orientation change. Here I found a very nice example how to work with AsyncTask and ProgressDialog.

One thing to consider is whether the result of the AsyncTask should be available only to the activity that started the task. If yes, then Romain Guy's answer is best. If it should be available to other activities of your application, then in onPostExecute you can use LocalBroadcastManager.

LocalBroadcastManager.getInstance(getContext()).sendBroadcast(new Intent("finished"));

You will also need to make sure that activity correctly handles situation when broadcast is sent while activity is paused.

The most proper way to this is to use a fragment to retain the instance of the async task, over rotations.

Here is a link to very simple example making it easy to follow integrate this technique into your apps.

https://gist.github.com/daichan4649/2480065

Have a look at this post. This Post involves AsyncTask performing long running operation and memory leak when screen rotation happens both in one sample application. The sample app is available on the source forge

Android : background processing/Async Opeartion with configuration change

To maintain the states of async opeartion during background process: you can take an help of fragments.

See the following steps :

Step 1: Create a headerless fragment let say background task and add a private async task class with in it.

Step 2 (Optional Step): if you want to put a loading cursor on top of your activity use below code:

Step 3: In your main Activity implement BackgroundTaskCallbacks interface defined in step 1

class BackgroundTask extends Fragment {
public BackgroundTask() {


}


// Add a static interface


static interface BackgroundTaskCallbacks {
void onPreExecute();


void onCancelled();


void onPostExecute();


void doInBackground();
}


private BackgroundTaskCallbacks callbacks;
private PerformAsyncOpeation asyncOperation;
private boolean isRunning;
private final String TAG = BackgroundTask.class.getSimpleName();


/**
* Start the async operation.
*/
public void start() {
Log.d(TAG, "********* BACKGROUND TASK START OPERATION ENTER *********");
if (!isRunning) {
asyncOperation = new PerformAsyncOpeation();
asyncOperation.execute();
isRunning = true;
}
Log.d(TAG, "********* BACKGROUND TASK START OPERATION EXIT *********");
}


/**
* Cancel the background task.
*/
public void cancel() {
Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION ENTER *********");
if (isRunning) {
asyncOperation.cancel(false);
asyncOperation = null;
isRunning = false;
}
Log.d(TAG, "********* BACKGROUND TASK CANCEL OPERATION EXIT *********");
}


/**
* Returns the current state of the background task.
*/
public boolean isRunning() {
return isRunning;
}


/**
* Android passes us a reference to the newly created Activity by calling
* this method after each configuration change.
*/
public void onAttach(Activity activity) {
Log.d(TAG, "********* BACKGROUND TASK ON ATTACH ENTER *********");
super.onAttach(activity);
if (!(activity instanceof BackgroundTaskCallbacks)) {
throw new IllegalStateException(
"Activity must implement the LoginCallbacks interface.");
}


// Hold a reference to the parent Activity so we can report back the
// task's
// current progress and results.
callbacks = (BackgroundTaskCallbacks) activity;
Log.d(TAG, "********* BACKGROUND TASK ON ATTACH EXIT *********");
}


public void onCreate(Bundle savedInstanceState) {
Log.d(TAG, "********* BACKGROUND TASK ON CREATE ENTER *********");
super.onCreate(savedInstanceState);
// Retain this fragment across configuration changes.
setRetainInstance(true);
Log.d(TAG, "********* BACKGROUND TASK ON CREATE EXIT *********");
}


public void onDetach() {
super.onDetach();
callbacks = null;
}


private class PerformAsyncOpeation extends AsyncTask<Void, Void, Void> {
protected void onPreExecute() {
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE ENTER *********");
if (callbacks != null) {
callbacks.onPreExecute();
}
isRunning = true;
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON PRE EXECUTE EXIT *********");
}


protected Void doInBackground(Void... params) {
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND ENTER *********");
if (callbacks != null) {
callbacks.doInBackground();
}
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > DO IN BACKGROUND EXIT *********");
return null;
}


protected void onCancelled() {
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL ENTER *********");
if (callbacks != null) {
callbacks.onCancelled();
}
isRunning = false;
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON CANCEL EXIT *********");
}


protected void onPostExecute(Void ignore) {
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE ENTER *********");
if (callbacks != null) {
callbacks.onPostExecute();
}
isRunning = false;
Log.d(TAG,
"********* BACKGROUND TASK :-> ASYNC OPERATION :- > ON POST EXECUTE EXIT *********");
}
}


public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
setRetainInstance(true);
}


public void onStart() {
super.onStart();
}


public void onResume() {
super.onResume();
}


public void onPause() {
super.onPause();
}


public void onStop() {
super.onStop();
}

public class ProgressIndicator extends Dialog {


public ProgressIndicator(Context context, int theme) {
super(context, theme);
}


private ProgressBar progressBar;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
requestWindowFeature(Window.FEATURE_NO_TITLE);
setContentView(R.layout.progress_indicator);
this.setCancelable(false);
progressBar = (ProgressBar) findViewById(R.id.progressBar);
progressBar.getIndeterminateDrawable().setColorFilter(R.color.DarkBlue, android.graphics.PorterDuff.Mode.SCREEN);
}


@Override
public void show() {
super.show();
}


@Override
public void dismiss() {
super.dismiss();
}


@Override
public void cancel() {
super.cancel();
}

public class MyActivity extends FragmentActivity implements BackgroundTaskCallbacks,{


private static final String KEY_CURRENT_PROGRESS = "current_progress";


ProgressIndicator progressIndicator = null;


private final static String TAG = MyActivity.class.getSimpleName();


private BackgroundTask task = null;


@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(//"set your layout here");
initialize your views and widget here .............






FragmentManager fm = getSupportFragmentManager();
task = (BackgroundTask) fm.findFragmentByTag("login");


// If the Fragment is non-null, then it is currently being
// retained across a configuration change.
if (task == null) {
task = new BackgroundTask();
fm.beginTransaction().add(task, "login").commit();
}


// Restore saved state
if (savedInstanceState != null) {
Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON CREATE :: "
+ task.isRunning());
if (task.isRunning()) {
progressIndicator = new ProgressIndicator(this,
R.style.TransparentDialog);
if (progressIndicator != null) {
progressIndicator.show();
}
}
}
}


@Override
protected void onPause() {
// TODO Auto-generated method stub
super.onPause();


}


@Override
protected void onSaveInstanceState(Bundle outState) {
// save the current state of your operation here by saying this


super.onSaveInstanceState(outState);
Log.i(TAG, "KEY_CURRENT_PROGRESS_VALUE ON SAVE INSTANCE :: "
+ task.isRunning());
outState.putBoolean(KEY_CURRENT_PROGRESS, task.isRunning());
if (progressIndicator != null) {
progressIndicator.dismiss();
progressIndicator.cancel();
}
progressIndicator = null;
}




private void performOperation() {


if (!task.isRunning() && progressIndicator == null) {
progressIndicator = new ProgressIndicator(this,
R.style.TransparentDialog);
progressIndicator.show();
}
if (task.isRunning()) {
task.cancel();
} else {
task.start();
}
}




@Override
protected void onDestroy() {
super.onDestroy();
if (progressIndicator != null) {
progressIndicator.dismiss();
progressIndicator.cancel();
}
progressIndicator = null;
}


@Override
public void onPreExecute() {
Log.i(TAG, "CALLING ON PRE EXECUTE");
}


@Override
public void onCancelled() {
Log.i(TAG, "CALLING ON CANCELLED");
if (progressIndicator != null) {
progressIndicator.dismiss();
progressIndicator.cancel();


}


public void onPostExecute() {
Log.i(TAG, "CALLING ON POST EXECUTE");
if (progressIndicator != null) {
progressIndicator.dismiss();
progressIndicator.cancel();
progressIndicator = null;
}
}


@Override
public void doInBackground() {
// put your code here for background operation
}

}

My solution.

In my case i've got a chain of AsyncTasks with the same context. Activity had an access only to first one. To cancel any running task i did the following:

public final class TaskLoader {


private static AsyncTask task;


private TaskLoader() {
throw new UnsupportedOperationException();
}


public static void setTask(AsyncTask task) {
TaskLoader.task = task;
}


public static void cancel() {
TaskLoader.task.cancel(true);
}
}

Task doInBackground():

protected Void doInBackground(Params... params) {
TaskLoader.setTask(this);
....
}

Activity onStop() or onPause():

protected void onStop() {
super.onStop();
TaskLoader.cancel();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
final AddTask task = mAddTask;
if (task != null && task.getStatus() != UserTask.Status.FINISHED) {
final String bookId = task.getBookId();
task.cancel(true);


if (bookId != null) {
outState.putBoolean(STATE_ADD_IN_PROGRESS, true);
outState.putString(STATE_ADD_BOOK, bookId);
}


mAddTask = null;
}
}


@Override
protected void onRestoreInstanceState(Bundle savedInstanceState) {
super.onRestoreInstanceState(savedInstanceState);
if (savedInstanceState.getBoolean(STATE_ADD_IN_PROGRESS)) {
final String id = savedInstanceState.getString(STATE_ADD_BOOK);
if (!BooksManager.bookExists(getContentResolver(), id)) {
mAddTask = (AddTask) new AddTask().execute(id);
}
}
}

you can also add android:configChanges="keyboardHidden|orientation|screenSize"

to your manifest example i hope it help

 <application
android:name=".AppController"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/AppTheme">