Background task, progress dialog, orientation chan

2018-12-31 08:14发布

I download some data from internet in background thread (I use AsyncTask) and display a progress dialog while downloading. Orientation changes, Activity is restarted and then my AsyncTask is completed - I want to dismiss the progess dialog and start a new Activity. But calling dismissDialog sometimes throws an exception (probably because the Activity was destroyed and new Activity hasn't been started yet).

What is the best way to handle this kind of problem (updating UI from background thread that works even if user changes orientation)? Did someone from Google provide some "official solution"?

8条回答
何处买醉
2楼-- · 2018-12-31 08:17

Did someone from Google provide some "official solution"?

Yes.

The solution is more of an application architecture proposal rather that just some code.

They proposed 3 design patterns that allows an application to work in-sync with a server, regardless of the application state (it will work even if the user finishes the app, the user changes screen, the app gets terminated, every other possible state where a background data operation could be interrumpted, this covers it)

The proposal is explained in the Android REST client applications speech during Google I/O 2010 by Virgil Dobjanschi. It is 1 hour long, but it is extremely worth watching.

The basis of it is abstracting network operations to a Service that works independently to any Activity in the application. If you're working with databases, the use of ContentResolver and Cursor would give you an out-of-the-box Observer pattern that is convenient to update UI without any aditional logic, once you updated your local database with the fetched remote data. Any other after-operation code would be run via a callback passed to the Service (I use a ResultReceiver subclass for this).

Anyway, my explanation is actually pretty vague, you should definititely watch the speech.

查看更多
时光乱了年华
3楼-- · 2018-12-31 08:18

I've toiled for a week to find a solution to this dilemma without resorting to editing the manifest file. The assumptions for this solution are:

  1. You always need to use a progress dialog
  2. Only one task is performed at a time
  3. You need the task to persist when the phone is rotated and the progress dialog to be automatically dismisses.

Implementation

You will need to copy the two files found at the bottom of this post into your workspace. Just make sure that:

  1. All your Activitys should extend BaseActivity

  2. In onCreate(), super.onCreate() should be called after you initialize any members that need to be accessed by your ASyncTasks. Also, override getContentViewId() to provide the form layout id.

  3. Override onCreateDialog() like usual to create dialogs managed by the activity.

  4. See code below for a sample static inner class to make your AsyncTasks. You can store your result in mResult to access later.


final static class MyTask extends SuperAsyncTask<Void, Void, Void> {

    public OpenDatabaseTask(BaseActivity activity) {
        super(activity, MY_DIALOG_ID); // change your dialog ID here...
                                       // and your dialog will be managed automatically!
    }

    @Override
    protected Void doInBackground(Void... params) {

        // your task code

        return null;
    }

    @Override
    public boolean onAfterExecute() {
        // your after execute code
    }
}

And finally, to launch your new task:

mCurrentTask = new MyTask(this);
((MyTask) mCurrentTask).execute();

That's it! I hope this robust solution will help someone.

BaseActivity.java (organize imports yourself)

protected abstract int getContentViewId();

public abstract class BaseActivity extends Activity {
    protected SuperAsyncTask<?, ?, ?> mCurrentTask;
    public HashMap<Integer, Boolean> mDialogMap = new HashMap<Integer, Boolean>();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(getContentViewId());

        mCurrentTask = (SuperAsyncTask<?, ?, ?>) getLastNonConfigurationInstance();
        if (mCurrentTask != null) {
            mCurrentTask.attach(this);
            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
        mCurrentTask.postExecution();
            }
        }
    }

    @Override
    protected void onPrepareDialog(int id, Dialog dialog) {
    super.onPrepareDialog(id, dialog);

        mDialogMap.put(id, true);
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (mCurrentTask != null) {
            mCurrentTask.detach();

            if (mDialogMap.get((Integer) mCurrentTask.dialogId) != null
                && mDialogMap.get((Integer) mCurrentTask.dialogId)) {
                return mCurrentTask;
            }
        }

        return super.onRetainNonConfigurationInstance();
    }

    public void cleanupTask() {
        if (mCurrentTask != null) {
            mCurrentTask = null;
            System.gc();
        }
    }
}

SuperAsyncTask.java

public abstract class SuperAsyncTask<Params, Progress, Result> extends AsyncTask<Params, Progress, Result> {
    protected BaseActivity mActivity = null;
    protected Result mResult;
    public int dialogId = -1;

    protected abstract void onAfterExecute();

    public SuperAsyncTask(BaseActivity activity, int dialogId) {
        super();
        this.dialogId = dialogId;
        attach(activity);
    }

    @Override
    protected void onPreExecute() {
        super.onPreExecute();
        mActivity.showDialog(dialogId); // go polymorphism!
    }    

    protected void onPostExecute(Result result) {
        super.onPostExecute(result);
        mResult = result;

        if (mActivity != null &&
                mActivity.mDialogMap.get((Integer) dialogId) != null
                && mActivity.mDialogMap.get((Integer) dialogId)) {
            postExecution();
        }
    };

    public void attach(BaseActivity activity) {
        this.mActivity = activity;
    }

    public void detach() {
        this.mActivity = null;
    }

    public synchronized boolean postExecution() {
        Boolean dialogExists = mActivity.mDialogMap.get((Integer) dialogId);
        if (dialogExists != null || dialogExists) {
            onAfterExecute();
            cleanUp();
    }

    public boolean cleanUp() {
        mActivity.removeDialog(dialogId);
        mActivity.mDialogMap.remove((Integer) dialogId);
        mActivity.cleanupTask();
        detach();
        return true;
    }
}
查看更多
长期被迫恋爱
4楼-- · 2018-12-31 08:18

While Mark's (CommonsWare) answer does indeed work for orientation changes, it fails if the Activity is destroyed directly (like in the case of a phone call).

You can handle the orientation changes AND the rare destroyed Activity events by using an Application object to reference your ASyncTask.

There's an excellent explanation of the problem and the solution here:

Credit goes completely to Ryan for figuring this one out.

查看更多
几人难应
5楼-- · 2018-12-31 08:22

Step #1: Make your AsyncTask a static nested class, or an entirely separate class, just not an inner (non-static nested) class.

Step #2: Have the AsyncTask hold onto the Activity via a data member, set via the constructor and a setter.

Step #3: When creating the AsyncTask, supply the current Activity to the constructor.

Step #4: In onRetainNonConfigurationInstance(), return the AsyncTask, after detaching it from the original, now-going-away activity.

Step #5: In onCreate(), if getLastNonConfigurationInstance() is not null, cast it to your AsyncTask class and call your setter to associate your new activity with the task.

Step #6: Do not refer to the activity data member from doInBackground().

If you follow the above recipe, it will all work. onProgressUpdate() and onPostExecute() are suspended between the start of onRetainNonConfigurationInstance() and the end of the subsequent onCreate().

Here is a sample project demonstrating the technique.

Another approach is to ditch the AsyncTask and move your work into an IntentService. This is particularly useful if the work to be done may be long and should go on regardless of what the user does in terms of activities (e.g., downloading a large file). You can use an ordered broadcast Intent to either have the activity respond to the work being done (if it is still in the foreground) or raise a Notification to let the user know if the work has been done. Here is a blog post with more on this pattern.

查看更多
浮光初槿花落
6楼-- · 2018-12-31 08:24

you should call all activity actions using activity handler. So if you are in some thread you should create a Runnable and posted using Activitie's Handler. Otherwise your app will crash sometimes with fatal exception.

查看更多
爱死公子算了
7楼-- · 2018-12-31 08:26

This is my solution: https://github.com/Gotchamoh/Android-AsyncTask-ProgressDialog

Basically the steps are:

  1. I use onSaveInstanceState to save the task if it is still processing.
  2. In onCreate I get the task if it was saved.
  3. In onPause I discard the ProgressDialog if it is shown.
  4. In onResume I show the ProgressDialog if the task is still processing.
查看更多
登录 后发表回答