简体   繁体   English

Android碎片。在屏幕旋转或配置更改期间保留AsyncTask

[英]Android Fragments. Retaining an AsyncTask during screen rotation or configuration change

I'm working on a Smartphone / Tablet app, using only one APK, and loading resources as is needed depending on screen size, the best design choice seemed to be using Fragments via the ACL. 我正在使用智能手机/平板电脑应用程序,只使用一个APK,并根据屏幕大小根据需要加载资源,最佳设计选择似乎是通过ACL使用Fragments。

This app has been working fine until now being only activity based. 这个应用程序一直工作正常,直到现在只是基于活动。 This is a mock class of how I handle AsyncTasks and ProgressDialogs in the Activities in order to have them work even when the screen is rotated or a configuration change occurs mid communication. 这是一个模拟类,用于处理活动中的AsyncTasks和ProgressDialogs,以便在屏幕旋转或通信中发生配置更改时使它们工作。

I will not change the manifest to avoid recreation of the Activity, there are many reasons why I dont want to do it, but mainly because the official docs say it isnt recomended and I've managed without it this far, so please dont recomend that route. 我不会改变清单以避免重新创建活动,有很多原因我不想这样做,但主要是因为官方文档说它不是推荐的,而且我已经管理了这么远,所以请不要推荐路线。

public class Login extends Activity {

    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    @Override
    public void onCreate(Bundle icicle) {
        super.onCreate(icicle);
        setContentView(R.layout.login);
        //SETUP UI OBJECTS
        restoreAsyncTask();
    }

    @Override
    public Object onRetainNonConfigurationInstance() {
        if (pd != null) pd.dismiss();
        if (asyncLoginThread != null) return (asyncLoginThread);
        return super.onRetainNonConfigurationInstance();
    }

    private void restoreAsyncTask();() {
        pd = new ProgressDialog(Login.this);
        if (getLastNonConfigurationInstance() != null) {
            asyncLoginThread = (AsyncTask<String, Void, Boolean>) getLastNonConfigurationInstance();
            if (asyncLoginThread != null) {
                if (!(asyncLoginThread.getStatus()
                        .equals(AsyncTask.Status.FINISHED))) {
                    showProgressDialog();
                }
            }
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
        @Override
        protected Boolean doInBackground(String... args) {
            try {
                //Connect to WS, recieve a JSON/XML Response
                //Place it somewhere I can use it.
            } catch (Exception e) {
                return true;
            }
            return true;
        }

        protected void onPostExecute(Boolean result) {
            if (result) {
                pd.dismiss();
                //Handle the response. Either deny entry or launch new Login Succesful Activity
            }
        }
    }
}

This code is working fine, I have around 10.000 users without complaint, so it seemed logical to just copy this logic into the new Fragment Based Design, but, of course, it isnt working. 这段代码工作正常,我有大约10,000个用户没有抱怨,所以将这个逻辑复制到新的基于片段的设计似乎合乎逻辑,但是,当然,它不起作用。

Here is the LoginFragment: 这是LoginFragment:

public class LoginFragment extends Fragment {

    FragmentActivity parentActivity;
    static ProgressDialog pd;
    AsyncTask<String, Void, Boolean> asyncLoginThread;

    public interface OnLoginSuccessfulListener {
        public void onLoginSuccessful(GlobalContainer globalContainer);
    }

    public void onSaveInstanceState(Bundle outState){
        super.onSaveInstanceState(outState);
        //Save some stuff for the UI State
    }

    @Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setRetainInstance(true);
        //If I setRetainInstance(true), savedInstanceState is always null. Besides that, when loading UI State, a NPE is thrown when looking for UI Objects.
        parentActivity = getActivity();
    }

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);
        try {
            loginSuccessfulListener = (OnLoginSuccessfulListener) activity;
        } catch (ClassCastException e) {
            throw new ClassCastException(activity.toString() + " must implement OnLoginSuccessfulListener");
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState) {
        RelativeLayout loginLayout = (RelativeLayout) inflater.inflate(R.layout.login, container, false);
        return loginLayout;
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        //SETUP UI OBJECTS
        if(savedInstanceState != null){
            //Reload UI state. Im doing this properly, keeping the content of the UI objects, not the object it self to avoid memory leaks.
        }
    }

    public class LoginThread extends AsyncTask<String, Void, Boolean> {
            @Override
            protected Boolean doInBackground(String... args) {
                try {
                    //Connect to WS, recieve a JSON/XML Response
                    //Place it somewhere I can use it.
                } catch (Exception e) {
                    return true;
                }
                return true;
            }

            protected void onPostExecute(Boolean result) {
                if (result) {
                    pd.dismiss();
                    //Handle the response. Either deny entry or launch new Login Succesful Activity
                }
            }
        }
    }
}

I cant use onRetainNonConfigurationInstance() since it has to be called from the Activity and not the Fragment, same goes with getLastNonConfigurationInstance() . 我不能使用onRetainNonConfigurationInstance()因为它必须从Activity而不是Fragment调用,与getLastNonConfigurationInstance()相同。 I've read some similar questions here with no answer. 我在这里读过一些类似的问题而没有答案。

I understand that it might require some working around to get this stuff organized properly in fragments, that being said, I would like to maintain the same basic design logic. 据我所知,它可能需要一些解决方法才能将这些东西正确地组织成片段,据说,我希望保持相同的基本设计逻辑。

What would be the proper way to retain the AsyncTask during a configuration change, and if its still runing, show a progressDialog, taking into consideration that the AsyncTask is a inner class to the Fragment and it is the Fragment itself who invokes the AsyncTask.execute()? 在配置更改期间保留AsyncTask的正确方法是什么,如果它仍在运行,则显示progressDialog,同时考虑到AsyncTask是Fragment的内部类,而Fragment本身调用AsyncTask.execute ()?

Fragments can actually make this a lot easier. 碎片实际上可以使这更容易。 Just use the method Fragment.setRetainInstance(boolean) to have your fragment instance retained across configuration changes. 只需使用Fragment.setRetainInstance(boolean)方法,即可在配置更改中保留您的片段实例。 Note that this is the recommended replacement for Activity.onRetainnonConfigurationInstance() in the docs. 请注意,这是docs中Activity.onRetainnonConfigurationInstance()的推荐替代品。

If for some reason you really don't want to use a retained fragment, there are other approaches you can take. 如果由于某种原因你真的不想使用保留的片段,你可以采取其他方法。 Note that each fragment has a unique identifier returned by Fragment.getId() . 请注意,每个片段都有Fragment.getId()返回的唯一标识符。 You can also find out if a fragment is being torn down for a config change through Fragment.getActivity().isChangingConfigurations() . 您还可以通过Fragment.getActivity()。isChangingConfigurations()查看是否正在拆除片段以进行配置更改。 So, at the point where you would decide to stop your AsyncTask (in onStop() or onDestroy() most likely), you could for example check if the configuration is changing and if so stick it in a static SparseArray under the fragment's identifier, and then in your onCreate() or onStart() look to see if you have an AsyncTask in the sparse array available. 因此,在您决定停止AsyncTask(最有可能在onStop()或onDestroy()中)的情况下,您可以检查配置是否正在更改,如果是,则将其粘贴在片段标识符下的静态SparseArray中,然后在你的onCreate()或onStart()中查看你是否在稀疏数组中有一个AsyncTask。

I think you will enjoy my extremely comprehensive and working example detailed below. 我想你会喜欢我下面详细介绍的非常全面和有效的例子。

  1. Rotation works, and the dialog survives. 旋转工作,对话存活。
  2. You can cancel the task and dialog by pressing the back button (if you want this behaviour). 您可以通过按后退按钮取消任务和对话框(如果您需要此行为)。
  3. It uses fragments. 它使用片段。
  4. The layout of the fragment underneath the activity changes properly when the device rotates. 当设备旋转时,活动下方片段的布局会正确更改。
  5. There is a complete source code download and a precompiled APK so you can see if the behaviour is what you want. 有完整的源代码下载和预编译的APK,因此您可以查看行为是否符合您的要求。

Edit 编辑

As requested by Brad Larson I have reproduced most of the linked solution below. 根据Brad Larson的要求,我已经复制了下面的大部分链接解决方案。 Also since I posted it I have been pointed to AsyncTaskLoader . 自从我发布它以来,我一直指向AsyncTaskLoader I'm not sure it is totally applicable to the same problems, but you should check it out anyway. 我不确定它是否完全适用于同样的问题,但无论如何你应该检查一下。

Using AsyncTask with progress dialogs and device rotation. 使用AsyncTask进度对话框和设备轮换。

A working solution! 一个有效的解决方

I have finally got everything to work. 我终于把一切都搞定了。 My code has the following features: 我的代码具有以下功能:

  1. A Fragment whose layout changes with orientation. Fragment的布局随方向而变化。
  2. An AsyncTask in which you can do some work. 一个AsyncTask ,你可以在其中做一些工作。
  3. A DialogFragment which shows the progress of the task in a progress bar (not just an indeterminate spinner). 一个DialogFragment ,它在进度条中显示任务的进度(不仅仅是一个不确定的微调器)。
  4. Rotation works without interrupting the task or dismissing the dialog. 旋转工作不会中断任务或解除对话框。
  5. The back button dismisses the dialog and cancels the task (you can alter this behaviour fairly easily though). 后退按钮取消对话框并取消任务(您可以相当容易地改变此行为)。

I don't think that combination of workingness can be found anywhere else. 我认为在其他任何地方都找不到工作的组合。

The basic idea is as follows. 基本思路如下。 There is a MainActivity class which contains a single fragment - MainFragment . 有一个MainActivity类,它包含一个片段 - MainFragment MainFragment has different layouts for horizontal and vertical orientation, and setRetainInstance() is false so that the layout can change. MainFragment具有不同的水平和垂直方向布局,而setRetainInstance()为false,因此布局可以更改。 This means that when the device orientation is changed, both MainActivity and MainFragment are completely destroyed and recreated. 这意味着当更改设备方向时, MainActivityMainFragment都将被完全销毁并重新创建。

Separately we have MyTask (extended from AsyncTask ) which does all the work. 另外我们有MyTask (从AsyncTask扩展)完成所有工作。 We can't store it in MainFragment because that will be destroyed, and Google has deprecated using anything like setRetainNonInstanceConfiguration() . 我们无法将其存储在MainFragment因为它会被销毁,Google已经弃用了setRetainNonInstanceConfiguration()类的东西。 That isn't always available anyway and is an ugly hack at best. 无论如何,这并不总是可用,并且最好是一个丑陋的黑客。 Instead we will store MyTask in another fragment, a DialogFragment called TaskFragment . 相反,我们将存储MyTask在另一个片段, DialogFragment称为TaskFragment This fragment will have setRetainInstance() set to true, so as the device rotates this fragment isn't destroyed, and MyTask is retained. 片段 setRetainInstance()设置为true,因此当设备旋转时,此片段不会被破坏,并且MyTask被保留。

Finally we need to tell the TaskFragment who to inform when it is finished, and we do that using setTargetFragment(<the MainFragment>) when we create it. 最后,我们需要告诉TaskFragment在完成时通知谁,我们在创建它时使用setTargetFragment(<the MainFragment>)来实现。 When the device is rotated and the MainFragment is destroyed and a new instance is created, we use the FragmentManager to find the dialog (based on its tag) and do setTargetFragment(<the new MainFragment>) . 当旋转设备并销毁MainFragment并创建新实例时,我们使用FragmentManager查找对话框(基于其标记)并执行setTargetFragment(<the new MainFragment>) That's pretty much it. 这就是它。

There were two other things I needed to do: first cancel the task when the dialog is dismissed, and second set the dismiss message to null, otherwise the dialog is weirdly dismissed when the device is rotated. 我需要做的另外两件事:首先取消对话框取消时的任务,然后将dismiss消息设置为null,否则在旋转设备时对话框会被奇怪地解除。

The code 代码

I won't list the layouts, they are pretty obvious and you can find them in the project download below. 我不会列出布局,它们非常明显,您可以在下面的项目下载中找到它们。

MainActivity 主要活动

This is pretty straightforward. 这非常简单。 I added a callback into this activity so it knows when the task is finished, but you might not need that. 我在此活动中添加了回调,因此它知道任务何时完成,但您可能不需要。 Mainly I just wanted to show the fragment-activity callback mechanism because it's quite neat and you might not have seen it before. 主要是我只想显示片段活动回调机制,因为它非常整洁,你可能以前没有看过它。

public class MainActivity extends Activity implements MainFragment.Callbacks
{
    @Override
    public void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
    }
    @Override
    public void onTaskFinished()
    {
        // Hooray. A toast to our success.
        Toast.makeText(this, "Task finished!", Toast.LENGTH_LONG).show();
        // NB: I'm going to blow your mind again: the "int duration" parameter of makeText *isn't*
        // the duration in milliseconds. ANDROID Y U NO ENUM? 
    }
}

MainFragment MainFragment

It's long but worth it! 它很长但值得!

public class MainFragment extends Fragment implements OnClickListener
{
    // This code up to onDetach() is all to get easy callbacks to the Activity. 
    private Callbacks mCallbacks = sDummyCallbacks;

    public interface Callbacks
    {
        public void onTaskFinished();
    }
    private static Callbacks sDummyCallbacks = new Callbacks()
    {
        public void onTaskFinished() { }
    };

    @Override
    public void onAttach(Activity activity)
    {
        super.onAttach(activity);
        if (!(activity instanceof Callbacks))
        {
            throw new IllegalStateException("Activity must implement fragment's callbacks.");
        }
        mCallbacks = (Callbacks) activity;
    }

    @Override
    public void onDetach()
    {
        super.onDetach();
        mCallbacks = sDummyCallbacks;
    }

    // Save a reference to the fragment manager. This is initialised in onCreate().
    private FragmentManager mFM;

    // Code to identify the fragment that is calling onActivityResult(). We don't really need
    // this since we only have one fragment to deal with.
    static final int TASK_FRAGMENT = 0;

    // Tag so we can find the task fragment again, in another instance of this fragment after rotation.
    static final String TASK_FRAGMENT_TAG = "task";

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

        // At this point the fragment may have been recreated due to a rotation,
        // and there may be a TaskFragment lying around. So see if we can find it.
        mFM = getFragmentManager();
        // Check to see if we have retained the worker fragment.
        TaskFragment taskFragment = (TaskFragment)mFM.findFragmentByTag(TASK_FRAGMENT_TAG);

        if (taskFragment != null)
        {
            // Update the target fragment so it goes to this fragment instead of the old one.
            // This will also allow the GC to reclaim the old MainFragment, which the TaskFragment
            // keeps a reference to. Note that I looked in the code and setTargetFragment() doesn't
            // use weak references. To be sure you aren't leaking, you may wish to make your own
            // setTargetFragment() which does.
            taskFragment.setTargetFragment(this, TASK_FRAGMENT);
        }
    }

    @Override
    public View onCreateView(LayoutInflater inflater, ViewGroup container,
            Bundle savedInstanceState)
    {
        return inflater.inflate(R.layout.fragment_main, container, false);
    }

    @Override
    public void onViewCreated(View view, Bundle savedInstanceState)
    {
        super.onViewCreated(view, savedInstanceState);

        // Callback for the "start task" button. I originally used the XML onClick()
        // but it goes to the Activity instead.
        view.findViewById(R.id.taskButton).setOnClickListener(this);
    }

    @Override
    public void onClick(View v)
    {
        // We only have one click listener so we know it is the "Start Task" button.

        // We will create a new TaskFragment.
        TaskFragment taskFragment = new TaskFragment();
        // And create a task for it to monitor. In this implementation the taskFragment
        // executes the task, but you could change it so that it is started here.
        taskFragment.setTask(new MyTask());
        // And tell it to call onActivityResult() on this fragment.
        taskFragment.setTargetFragment(this, TASK_FRAGMENT);

        // Show the fragment.
        // I'm not sure which of the following two lines is best to use but this one works well.
        taskFragment.show(mFM, TASK_FRAGMENT_TAG);
//      mFM.beginTransaction().add(taskFragment, TASK_FRAGMENT_TAG).commit();
    }

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data)
    {
        if (requestCode == TASK_FRAGMENT && resultCode == Activity.RESULT_OK)
        {
            // Inform the activity. 
            mCallbacks.onTaskFinished();
        }
    }

TaskFragment TaskFragment

    // This and the other inner class can be in separate files if you like.
    // There's no reason they need to be inner classes other than keeping everything together.
    public static class TaskFragment extends DialogFragment
    {
        // The task we are running.
        MyTask mTask;
        ProgressBar mProgressBar;

        public void setTask(MyTask task)
        {
            mTask = task;

            // Tell the AsyncTask to call updateProgress() and taskFinished() on this fragment.
            mTask.setFragment(this);
        }

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

            // Retain this instance so it isn't destroyed when MainActivity and
            // MainFragment change configuration.
            setRetainInstance(true);

            // Start the task! You could move this outside this activity if you want.
            if (mTask != null)
                mTask.execute();
        }

        @Override
        public View onCreateView(LayoutInflater inflater, ViewGroup container,
                Bundle savedInstanceState)
        {
            View view = inflater.inflate(R.layout.fragment_task, container);
            mProgressBar = (ProgressBar)view.findViewById(R.id.progressBar);

            getDialog().setTitle("Progress Dialog");

            // If you're doing a long task, you probably don't want people to cancel
            // it just by tapping the screen!
            getDialog().setCanceledOnTouchOutside(false);

            return view;
        }

        // This is to work around what is apparently a bug. If you don't have it
        // here the dialog will be dismissed on rotation, so tell it not to dismiss.
        @Override
        public void onDestroyView()
        {
            if (getDialog() != null && getRetainInstance())
                getDialog().setDismissMessage(null);
            super.onDestroyView();
        }

        // Also when we are dismissed we need to cancel the task.
        @Override
        public void onDismiss(DialogInterface dialog)
        {
            super.onDismiss(dialog);
            // If true, the thread is interrupted immediately, which may do bad things.
            // If false, it guarantees a result is never returned (onPostExecute() isn't called)
            // but you have to repeatedly call isCancelled() in your doInBackground()
            // function to check if it should exit. For some tasks that might not be feasible.
            if (mTask != null) {
                mTask.cancel(false);
            }

            // You don't really need this if you don't want.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_CANCELED, null);
        }

        @Override
        public void onResume()
        {
            super.onResume();
            // This is a little hacky, but we will see if the task has finished while we weren't
            // in this activity, and then we can dismiss ourselves.
            if (mTask == null)
                dismiss();
        }

        // This is called by the AsyncTask.
        public void updateProgress(int percent)
        {
            mProgressBar.setProgress(percent);
        }

        // This is also called by the AsyncTask.
        public void taskFinished()
        {
            // Make sure we check if it is resumed because we will crash if trying to dismiss the dialog
            // after the user has switched to another app.
            if (isResumed())
                dismiss();

            // If we aren't resumed, setting the task to null will allow us to dimiss ourselves in
            // onResume().
            mTask = null;

            // Tell the fragment that we are done.
            if (getTargetFragment() != null)
                getTargetFragment().onActivityResult(TASK_FRAGMENT, Activity.RESULT_OK, null);
        }
    }

MyTask MyTask

    // This is a fairly standard AsyncTask that does some dummy work.
    public static class MyTask extends AsyncTask<Void, Void, Void>
    {
        TaskFragment mFragment;
        int mProgress = 0;

        void setFragment(TaskFragment fragment)
        {
            mFragment = fragment;
        }

        @Override
        protected Void doInBackground(Void... params)
        {
            // Do some longish task. This should be a task that we don't really
            // care about continuing
            // if the user exits the app.
            // Examples of these things:
            // * Logging in to an app.
            // * Downloading something for the user to view.
            // * Calculating something for the user to view.
            // Examples of where you should probably use a service instead:
            // * Downloading files for the user to save (like the browser does).
            // * Sending messages to people.
            // * Uploading data to a server.
            for (int i = 0; i < 10; i++)
            {
                // Check if this has been cancelled, e.g. when the dialog is dismissed.
                if (isCancelled())
                    return null;

                SystemClock.sleep(500);
                mProgress = i * 10;
                publishProgress();
            }
            return null;
        }

        @Override
        protected void onProgressUpdate(Void... unused)
        {
            if (mFragment == null)
                return;
            mFragment.updateProgress(mProgress);
        }

        @Override
        protected void onPostExecute(Void unused)
        {
            if (mFragment == null)
                return;
            mFragment.taskFinished();
        }
    }
}

Download the example project 下载示例项目

Here is the source code and the APK . 这是源代码APK Sorry, the ADT insisted on adding the support library before it would let me make a project. 对不起,ADT坚持要先添加支持库才能让我创建一个项目。 I'm sure you can remove it. 我相信你可以删除它。

I've recently posted an article describing how to handle configuration changes using retained Fragment s. 我最近发布了一篇文章,描述了如何使用保留的Fragment来处理配置更改。 It solves the problem of retaining an AsyncTask across a rotation change nicely. 它解决了在旋转变化中很好地保留AsyncTask的问题。

The TL;DR is to use host your AsyncTask inside a Fragment , call setRetainInstance(true) on the Fragment , and report the AsyncTask 's progress/results back to it's Activity (or it's target Fragment , if you choose to use the approach described by @Timmmm) through the retained Fragment . 该TL; DR是使用承载您AsyncTask一个内部Fragment ,调用setRetainInstance(true)Fragment ,而该报告AsyncTask到它的进步/结果反馈Activity (或者它的目标Fragment ,如果你选择使用描述的方法通过@Timmmm)通过保留的Fragment

My first suggestion is to avoid inner AsyncTasks , you can read a question that I asked about this and the answers: Android: AsyncTask recommendations: private class or public class? 我的第一个建议是避免使用内部AsyncTasks ,你可以阅读我询问的问题及答案: Android:AsyncTask建议:私有类还是公共类?

After that i started using non-inner and... now i see A LOT of benefits. 在那之后我开始使用非内部和......现在我看到了很多好处。

The second is, keep a reference of your running AsyncTask in the Application Class - http://developer.android.com/reference/android/app/Application.html 第二个是,在Application类中保留正在运行的AsyncTask的引用 - http://developer.android.com/reference/android/app/Application.html

Everytime you start an AsyncTask, set it on the Application and when it finishes it set it to null. 每次启动AsyncTask时,在Application上设置它,当它完成时将其设置为null。

When a fragment/activity starts you can check if any AsyncTask is running (by checking if it's null or not on the Application) and then set the reference inside to whatever you want (activity, fragment etc so you can do callbacks). 当一个片段/活动开始时,您可以检查是否有任何AsyncTask正在运行(通过检查它是否在应用程序上为null),然后将内部引用设置为您想要的任何内容(活动,片段等,以便您可以进行回调)。

This will solve your problem: If you only have 1 AsyncTask running at any determined time you can add a simple reference: 这将解决您的问题:如果您在任何确定的时间只运行1个AsyncTask,您可以添加一个简单的引用:

AsyncTask<?,?,?> asyncTask = null;

Else, have in the Aplication a HashMap with references to them. 另外,在Aplication中有一个HashMap,引用它们。

The progress dialog can follow the exact same principle. 进度对话框可以遵循完全相同的原则。

I came up with a method of using AsyncTaskLoaders for this. 我想出了一种使用AsyncTaskLoaders的方法。 It's pretty easy to use and requires less overhead IMO.. 它非常易于使用,并且需要较少的开销IMO ..

Basically you create an AsyncTaskLoader like this: 基本上你创建一个像这样的AsyncTaskLoader:

public class MyAsyncTaskLoader extends AsyncTaskLoader {
    Result mResult;
    public HttpAsyncTaskLoader(Context context) {
        super(context);
    }

    protected void onStartLoading() {
        super.onStartLoading();
        if (mResult != null) {
            deliverResult(mResult);
        }
        if (takeContentChanged() ||  mResult == null) {
            forceLoad();
        }
    }

    @Override
    public Result loadInBackground() {
        SystemClock.sleep(500);
        mResult = new Result();
        return mResult;
    }
}

Then in your activity that uses the above AsyncTaskLoader when a button is clicked: 然后在单击按钮时使用上述AsyncTaskLoader的活动中:

public class MyActivityWithBackgroundWork extends FragmentActivity implements LoaderManager.LoaderCallbacks<Result> {

    private String username,password;       
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        // TODO Auto-generated method stub
        super.onCreate(savedInstanceState);
        setContentView(R.layout.mylayout);
        //this is only used to reconnect to the loader if it already started
        //before the orientation changed
        Loader loader = getSupportLoaderManager().getLoader(0);
        if (loader != null) {
            getSupportLoaderManager().initLoader(0, null, this);
        }
    }

    public void doBackgroundWorkOnClick(View button) {
        //might want to disable the button while you are doing work
        //to prevent user from pressing it again.

        //Call resetLoader because calling initLoader will return
        //the previous result if there was one and we may want to do new work
        //each time
        getSupportLoaderManager().resetLoader(0, null, this);
    }   


    @Override
    public Loader<Result> onCreateLoader(int i, Bundle bundle) {
        //might want to start a progress bar
        return new MyAsyncTaskLoader(this);
    }


    @Override
    public void onLoadFinished(Loader<LoginResponse> loginLoader,
                               LoginResponse loginResponse)
    {
        //handle result
    }

    @Override
    public void onLoaderReset(Loader<LoginResponse> responseAndJsonHolderLoader)
    {
        //remove references to previous loader resources

    }
}

This seems to handle orientation changes fine and your background task will continue during the rotation. 这似乎可以很好地处理方向更改,并且您的后台任务将在轮换期间继续。

A few things to note: 有几点需要注意:

  1. If in onCreate you reattach to the asynctaskloader you will get called back in onLoadFinished() with the previous result (even if you had already been told the request was complete). 如果在onCreate中你重新连接到asynctaskloader,你将在onLoadFinished()中调用前一个结果(即使你已经被告知请求已经完成)。 This is actually good behavior most of the time but sometimes it can be tricky to handle. 这在大多数情况下实际上是很好的行为,但有时处理起来可能很棘手。 While I imagine there are lots of ways to handle this what I did was I called loader.abandon() in onLoadFinished. 虽然我想有很多方法可以解决这个问题,但我在onLoadFinished中调用了loader.abandon()。 Then I added check in onCreate to only reattach to the loader if it wasn't already abandoned. 然后我添加了check on onCreate以仅重新附加到加载器(如果尚未放弃)。 If you need the resulting data again you won't want to do that. 如果您再次需要结果数据,则不希望这样做。 In most cases you want the data. 在大多数情况下,您需要数据。

I have more details on using this for http calls here 我在这里有更多关于将此用于http调用的详细信息

I created a very tiny open-source background task library which is heavily based on the Marshmallow AsyncTask but with additional functionality such as: 我创建了一个非常小的开源后台任务库,它基于Marshmallow AsyncTask但具有额外的功能,例如:

  1. Automatically retaining tasks across configuration changes; 跨配置更改自动保留任务;
  2. UI callback (listeners); UI回调(监听器);
  3. Doesn't restart or cancel task when the device rotates (like Loaders would do); 设备旋转时不重启或取消任务(如装载机那样);

The library internally uses a Fragment without any user interface, which is retained accross configuration changes ( setRetainInstance(true) ). 该库内部使用Fragment而没有任何用户界面,该界面在配置更改时保留( setRetainInstance(true) )。

You can find it on GitHub: https://github.com/NeoTech-Software/Android-Retainable-Tasks 你可以在GitHub上找到它: https//github.com/NeoTech-Software/Android-Retainable-Tasks

Most basic example (version 0.2.0): 最基本的例子(版本0.2.0):

This example fully retains the task, using a very limited amount of code. 此示例使用非常有限的代码完全保留任务。

Task: 任务:

private class ExampleTask extends Task<Integer, String> {

    public ExampleTask(String tag){
        super(tag);
    }

    protected String doInBackground() {
        for(int i = 0; i < 100; i++) {
            if(isCancelled()){
                break;
            }
            SystemClock.sleep(50);
            publishProgress(i);
        }
        return "Result";
    }
}

Activity: 活动:

public class Main extends TaskActivityCompat implements Task.Callback {

    @Override
    public void onClick(View view){
        ExampleTask task = new ExampleTask("activity-unique-tag");
        getTaskManager().execute(task, this);
    }

    @Override
    public Task.Callback onPreAttach(Task<?, ?> task) {
        //Restore the user-interface based on the tasks state
        return this; //This Activity implements Task.Callback
    }

    @Override
    public void onPreExecute(Task<?, ?> task) {
        //Task started
    }

    @Override
    public void onPostExecute(Task<?, ?> task) {
        //Task finished
        Toast.makeText(this, "Task finished", Toast.LENGTH_SHORT).show();
    }
}

If anyone finds their way to this thread then I found a clean approach was to run the Async task from an app.Service (started with START_STICKY) and then on recreate iterate over the running services to find out whether the service (and hence async task) is still running; 如果有人找到了通往这个线程的方法,那么我发现一个干净的方法是从app.Service (以START_STICKY开始)运行Async任务,然后重新创建迭代运行的服务以查明是否服务(以及因此异步任务) )仍然在运行;

    public boolean isServiceRunning(String serviceClassName) {
    final ActivityManager activityManager = (ActivityManager) Application.getContext().getSystemService(Context.ACTIVITY_SERVICE);
    final List<RunningServiceInfo> services = activityManager.getRunningServices(Integer.MAX_VALUE);

    for (RunningServiceInfo runningServiceInfo : services) {
        if (runningServiceInfo.service.getClassName().equals(serviceClassName)){
            return true;
        }
    }
    return false;
 }

If it is, re-add the DialogFragment (or whatever) and if it is not ensure the dialog has been dismissed. 如果是,请重新添加DialogFragment (或其他),如果不是,请确保对话框已被解除。

This is particularly pertinent if you are using the v4.support.* libraries since (at the time of writing) they have know issues with the setRetainInstance method and view paging. 如果您使用的是v4.support.*库,那么这一点尤其重要,因为(在撰写本文时)他们已经知道setRetainInstance方法和查看分页的问题。 Furthermore, by not retaining the instance you can recreate your activity using a different set of resources (ie a different view layout for the new orientation) 此外,通过不保留实例,您可以使用不同的资源集重新创建活动(即新方向的不同视图布局)

My approach is to use delegation design pattern, in general, we can isolate the actual business logic (read data from internet or database or whatsoever) from AsyncTask (the delegator) to BusinessDAO (the delegate), in your AysncTask.doInBackground() method, delegate the actual task to BusinessDAO, then implement a singleton process mechanism in BusinessDAO, so that multiple call to BusinessDAO.doSomething() will just trigger one actual task running each time and waiting for the task result. 我的方法是使用委托设计模式,通常,我们可以在AysncTask.doInBackground()方法中将实际业务逻辑(从Internet或数据库中读取数据或任何其他内容)从AsyncTask(委托者)到BusinessDAO(委托)隔离开来,将实际任务委托给BusinessDAO,然后在BusinessDAO中实现单例进程机制,这样多次调用BusinessDAO.doSomething()就会触发每次运行的一个实际任务并等待任务结果。 The idea is retain the delegate (ie BusinessDAO) during the configuration change, instead of the delegator (ie AsyncTask). 这个想法是在配置更改期间保留委托(即BusinessDAO),而不是委托者(即AsyncTask)。

  1. Create/Implement our own Application, the purpose is to create/initialize BusinessDAO here, so that our BusinessDAO's lifecycle is application scoped, not activity scoped, note that you need change AndroidManifest.xml to use MyApplication: 创建/实现我们自己的应用程序,目的是在这里创建/初始化BusinessDAO,以便我们的BusinessDAO的生命周期是应用程序作用域,而不是活动作用域,请注意您需要更改AndroidManifest.xml以使用MyApplication:

     public class MyApplication extends android.app.Application { private BusinessDAO businessDAO; @Override public void onCreate() { super.onCreate(); businessDAO = new BusinessDAO(); } pubilc BusinessDAO getBusinessDAO() { return businessDAO; } } 
  2. Our existing Activity/Fragement are mostly unchanged, still implement AsyncTask as an inner class and involve AsyncTask.execute() from Activity/Fragement, the difference now is AsyncTask will delegate the actual task to BusinessDAO, so during the configuration change, a second AsyncTask will be initialized and executed, and call BusinessDAO.doSomething() second time, however, second call to BusinessDAO.doSomething() will not trigger a new running task, instead, waiting for current running task to finish: 我们现有的Activity / Fragement大部分没有变化,仍然将AsyncTask实现为内部类并且涉及来自Activity / Fragement的AsyncTask.execute(),现在的区别是AsyncTask会将实际任务委托给BusinessDAO,所以在配置更改期间,第二个AsyncTask将被初始化并执行,并第二次调用BusinessDAO.doSomething(),但是,第二次调用BusinessDAO.doSomething()将不会触发新的运行任务,而是等待当前运行任务完成:

     public class LoginFragment extends Fragment { ... ... public class LoginAsyncTask extends AsyncTask<String, Void, Boolean> { // get a reference of BusinessDAO from application scope. BusinessDAO businessDAO = ((MyApplication) getApplication()).getBusinessDAO(); @Override protected Boolean doInBackground(String... args) { businessDAO.doSomething(); return true; } protected void onPostExecute(Boolean result) { //Handle task result and update UI stuff. } } ... ... } 
  3. Inside BusinessDAO, implement singleton process mechanism, for example: 在BusinessDAO内部,实现单例进程机制,例如:

     public class BusinessDAO { ExecutorCompletionService<MyTask> completionExecutor = new ExecutorCompletionService<MyTask(Executors.newFixedThreadPool(1)); Future<MyTask> myFutureTask = null; public void doSomething() { if (myFutureTask == null) { // nothing running at the moment, submit a new callable task to run. MyTask myTask = new MyTask(); myFutureTask = completionExecutor.submit(myTask); } // Task already submitted and running, waiting for the running task to finish. myFutureTask.get(); } // If you've never used this before, Callable is similar with Runnable, with ability to return result and throw exception. private class MyTask extends Callable<MyTask> { public MyAsyncTask call() { // do your job here. return this; } } } 

I am not 100% sure if this will work, moreover, the sample code snippet should be considered as pseudocode. 我不是100%确定这是否可行,而且,示例代码段应被视为伪代码。 I am just trying to give you some clue from design level. 我只是想从设计层面给你一些线索。 Any feedback or suggestions are welcome and appreciated. 任何反馈或建议都欢迎和赞赏。

You could make the AsyncTask a static field. 您可以使AsyncTask成为静态字段。 If you need a context, you should ship your application context. 如果您需要上下文,则应该发送应用程序上下文。 This will avoid memory leaks, otherwise you'd keep a reference to your entire activity. 这样可以避免内存泄漏,否则您将保留对整个活动的引用。

I write samepl code to solve this problem 我写了相同的代码来解决这个问题

First step is make Application class: 第一步是make类:

public class TheApp extends Application {

private static TheApp sTheApp;
private HashMap<String, AsyncTask<?,?,?>> tasks = new HashMap<String, AsyncTask<?,?,?>>();

@Override
public void onCreate() {
    super.onCreate();
    sTheApp = this;
}

public static TheApp get() {
    return sTheApp;
}

public void registerTask(String tag, AsyncTask<?,?,?> task) {
    tasks.put(tag, task);
}

public void unregisterTask(String tag) {
    tasks.remove(tag);
}

public AsyncTask<?,?,?> getTask(String tag) {
    return tasks.get(tag);
}
}

In AndroidManifest.xml 在AndroidManifest.xml中

<application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/AppTheme"
        android:name="com.example.tasktest.TheApp">

Code in activity: 活动代码:

public class MainActivity extends Activity {

private Task1 mTask1;

@Override
protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.activity_main);

    mTask1 = (Task1)TheApp.get().getTask("task1");

}

/*
 * start task is not running jet
 */
public void handletask1(View v) {
    if (mTask1 == null) {
        mTask1 = new Task1();
        TheApp.get().registerTask("task1", mTask1);
        mTask1.execute();
    } else
        Toast.makeText(this, "Task is running...", Toast.LENGTH_SHORT).show();

}

/*
 * cancel task if is not finished
 */
public void handelCancel(View v) {
    if (mTask1 != null)
        mTask1.cancel(false);
}

public class Task1 extends AsyncTask<Void, Void, Void>{

    @Override
    protected Void doInBackground(Void... params) {
        try {
            for(int i=0; i<120; i++) {
                Thread.sleep(1000);
                Log.i("tests", "loop=" + i);
                if (this.isCancelled()) {
                    Log.e("tests", "tssk cancelled");
                    break;
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    protected void onCancelled(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }

    @Override
    protected void onPostExecute(Void result) {
        TheApp.get().unregisterTask("task1");
        mTask1 = null;
    }
}

}

When activity orientation changes variable mTask is inited from app context. 当活动方向更改变量时,mTask将从应用程序上下文中获取。 When task is finished variable is set to null and remove from memory. 任务完成时,变量设置为null并从内存中删除。

For me its enough. 对我来说足够了。

Have a look at below example , how to use retained fragment to retain background task: 看看下面的例子,如何使用保留片段来保留后台任务:

public class NetworkRequestFragment extends Fragment {

    // Declare some sort of interface that your AsyncTask will use to communicate with the Activity
    public interface NetworkRequestListener {
        void onRequestStarted();
        void onRequestProgressUpdate(int progress);
        void onRequestFinished(SomeObject result);
    }

    private NetworkTask mTask;
    private NetworkRequestListener mListener;

    private SomeObject mResult;

    @Override
    public void onAttach(Activity activity) {
        super.onAttach(activity);

        // Try to use the Activity as a listener
        if (activity instanceof NetworkRequestListener) {
            mListener = (NetworkRequestListener) activity;
        } else {
            // You can decide if you want to mandate that the Activity implements your callback interface
            // in which case you should throw an exception if it doesn't:
            throw new IllegalStateException("Parent activity must implement NetworkRequestListener");
            // or you could just swallow it and allow a state where nobody is listening
        }
    }

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

        // Retain this Fragment so that it will not be destroyed when an orientation
        // change happens and we can keep our AsyncTask running
        setRetainInstance(true);
    }

    /**
     * The Activity can call this when it wants to start the task
     */
    public void startTask(String url) {
        mTask = new NetworkTask(url);
        mTask.execute();
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // If the AsyncTask finished when we didn't have a listener we can
        // deliver the result here
        if ((mResult != null) && (mListener != null)) {
            mListener.onRequestFinished(mResult);
            mResult = null;
        }
    }

    @Override
    public void onDestroy() {
        super.onDestroy();

        // We still have to cancel the task in onDestroy because if the user exits the app or
        // finishes the Activity, we don't want the task to keep running
        // Since we are retaining the Fragment, onDestroy won't be called for an orientation change
        // so this won't affect our ability to keep the task running when the user rotates the device
        if ((mTask != null) && (mTask.getStatus == AsyncTask.Status.RUNNING)) {
            mTask.cancel(true);
        }
    }

    @Override
    public void onDetach() {
        super.onDetach();

        // This is VERY important to avoid a memory leak (because mListener is really a reference to an Activity)
        // When the orientation change occurs, onDetach will be called and since the Activity is being destroyed
        // we don't want to keep any references to it
        // When the Activity is being re-created, onAttach will be called and we will get our listener back
        mListener = null;
    }

    private class NetworkTask extends AsyncTask<String, Integer, SomeObject> {

        @Override
        protected void onPreExecute() {
            if (mListener != null) {
                mListener.onRequestStarted();
            }
        }

        @Override
        protected SomeObject doInBackground(String... urls) {
           // Make the network request
           ...
           // Whenever we want to update our progress:
           publishProgress(progress);
           ...
           return result;
        }

        @Override
        protected void onProgressUpdate(Integer... progress) {
            if (mListener != null) {
                mListener.onRequestProgressUpdate(progress[0]);
            }
        }

        @Override
        protected void onPostExecute(SomeObject result) {
            if (mListener != null) {
                mListener.onRequestFinished(result);
            } else {
                // If the task finishes while the orientation change is happening and while
                // the Fragment is not attached to an Activity, our mListener might be null
                // If you need to make sure that the result eventually gets to the Activity
                // you could save the result here, then in onActivityCreated you can pass it back
                // to the Activity
                mResult = result;
            }
        }

    }
}

Have a look here . 看看这里

There is a solution based on Timmmm's solution. 有一个基于Timmmm解决方案解决方案。

But I improved it: 但我改进了它:

  • Now the solution is extendable - you only need to extend FragmentAbleToStartTask 现在解决方案是可扩展的 - 您只需要扩展FragmentAbleToStartTask

  • You able to keep running several tasks at the same time. 您可以同时继续运行多个任务。

    And in my opinion it's as easy as startActivityForResult and receive result 在我看来,它就像startActivityForResult一样容易并且收到结果

  • You also can stop a running task and check whether particular task is running 您还可以停止正在运行的任务并检查特定任务是否正在运行

Sorry for my English 对不起我的英语不好

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM