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

bli*_*uff 84 android progressdialog android-asynctask android-fragments android-support-library

我正在使用智能手机/平板电脑应用程序,只使用一个APK,并根据屏幕大小根据需要加载资源,最佳设计选择似乎是通过ACL使用Fragments.

这个应用程序一直工作正常,直到现在只是基于活动.这是一个模拟类,用于处理活动中的AsyncTasks和ProgressDialogs,以便在屏幕旋转或通信中发生配置更改时使它们工作.

我不会改变清单以避免重新创建活动,有很多原因我不想这样做,但主要是因为官方文档说它不是推荐的,而且我已经管理了这么远,所以请不要推荐路线.

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
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

这段代码工作正常,我有大约10,000个用户没有抱怨,所以将这个逻辑复制到新的基于片段的设计似乎合乎逻辑,但是,当然,它不起作用.

这是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
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

我不能使用,onRetainNonConfigurationInstance()因为它必须从Activity而不是Fragment调用,同样如此getLastNonConfigurationInstance().我在这里读过一些类似的问题而没有答案.

据我所知,它可能需要一些解决方法才能将这些东西正确地组织成片段,据说,我希望保持相同的基本设计逻辑.

在配置更改期间保留AsyncTask的正确方法是什么,如果它仍在运行,则显示progressDialog,同时考虑到AsyncTask是Fragment的内部类,而Fragment本身调用AsyncTask.execute ()?

hac*_*bod 75

碎片实际上可以使这更容易.只需使用Fragment.setRetainInstance(boolean)方法,即可在配置更改中保留您的片段实例.请注意,这是docs中Activity.onRetainnonConfigurationInstance()的推荐替代品.

如果由于某种原因你真的不想使用保留的片段,你可以采取其他方法.请注意,每个片段都有Fragment.getId()返回的唯一标识符.您还可以通过Fragment.getActivity().isChangingConfigurations()查看是否正在拆除片段以进行配置更改.因此,点在哪里,你会决定停止您的AsyncTask(中的onStop()或的onDestroy()最有可能的),例如,您可以检查配置是否正在发生变化,如果是坚持在静态SparseArray片段的标识符下,然后在你的onCreate()或onStart()中查看你是否在稀疏数组中有一个AsyncTask.

  • @jakk活动,片段等的生命周期方法由主GUI线程的消息队列按顺序调用,因此即使任务在这些生命周期方法完成(甚至调用)之前在后台同时完成,`onPostExecute`方法也会仍然需要等待最终由主线程的消息队列处理. (6认同)
  • 是不是AsyncTask可能会在保留的Fragment的onCreateView运行之前将其结果发回? (4认同)

Tim*_*mmm 66

我想你会喜欢我下面详细介绍的非常全面和有效的例子.

  1. 旋转工作,对话存活.
  2. 您可以通过按后退按钮取消任务和对话框(如果您需要此行为).
  3. 它使用片段.
  4. 当设备旋转时,活动下方片段的布局会正确更改.
  5. 有完整的源代码下载和预编译的APK,因此您可以查看行为是否符合您的要求.

编辑

根据Brad Larson的要求,我已经复制了下面的大部分链接解决方案.自从我发布以来,我也被指出了AsyncTaskLoader.我不确定它是否完全适用于同样的问题,但无论如何你应该检查一下.

使用AsyncTask进度对话框和设备旋转.

一个有效的解决方

我终于把一切都搞定了.我的代码具有以下功能:

  1. A Fragment的布局随方向而变化.
  2. 一个AsyncTask可以在其中做一些工作.
  3. A DialogFragment显示进度条中的任务进度(不仅仅是一个不确定的微调器).
  4. 旋转工作不会中断任务或解除对话框.
  5. 后退按钮取消对话框并取消任务(您可以相当容易地改变此行为).

我认为在其他任何地方都找不到工作的组合.

基本思路如下.有一个MainActivity类包含一个片段 - MainFragment.MainFragment具有不同的水平和垂直方向布局,并且setRetainInstance()是错误的,因此布局可以更改.这意味着,当设备方向改变时,都MainActivityMainFragment被完全破坏并重新创建.

另外,我们MyTask(扩展AsyncTask)完成所有工作.我们无法存储它,MainFragment因为它会被销毁,谷歌已经弃用了类似的东西setRetainNonInstanceConfiguration().无论如何,这并不总是可用,并且最好是一个丑陋的黑客.相反,我们将存储MyTask在另一个片段中,一个DialogFragment名为TaskFragment.这个片段setRetainInstance()设置为true,以便在设备旋转该片段不被破坏,而MyTask被保留.

最后,我们需要告诉TaskFragment谁在完成时告知谁,我们setTargetFragment(<the MainFragment>)在创建它时使用它.当设备旋转并被MainFragment销毁并创建一个新实例时,我们使用它FragmentManager来查找对话框(基于其标签)并执行setTargetFragment(<the new MainFragment>).这就是它.

我需要做的另外两件事:首先取消对话框取消时的任务,然后将dismiss消息设置为null,否则在旋转设备时对话框会被奇怪地解除.

代码

我不会列出布局,它们非常明显,您可以在下面的项目下载中找到它们.

主要活动

这非常简单.我在此活动中添加了回调,因此它知道任务何时完成,但您可能不需要.主要是我只想显示片段活动回调机制,因为它非常整洁,你可能以前没有看过它.

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? 
    }
}
Run Code Online (Sandbox Code Playgroud)

MainFragment

它很长但值得!

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();
        }
    }
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
Run Code Online (Sandbox Code Playgroud)

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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

下载示例项目

这是源代码APK.对不起,ADT坚持要先添加支持库才能让我创建一个项目.我相信你可以删除它.

  • 我会避免保留进度条`DialogFragment`,因为它有UI元素,可以保存对旧上下文的引用.相反,我将`AsyncTask`存储在另一个空片段中,并将`DialogFragment`设置为其目标. (4认同)
  • 无法下载源代码和APK. (2认同)

Ale*_*ood 16

我最近发布了一篇文章,描述了如何使用retain Fragments 处理配置更改.它解决了保持AsyncTask旋转变化很好的问题.

TL; DR是使用你的AsyncTask内部主机a Fragment,调用setRetainInstance(true)Fragment,并通过保留AsyncTask来报告它的进度/结果Activity(或者它的目标Fragment,如果你选择使用@Timmmm描述的方法)Fragment.

  • 您将如何处理嵌套碎片?就像AsyncTask从另一个片段(Tab)中的RetainedFragment开始一样. (5认同)

net*_*ein 13

我的第一个建议是避免使用内部AsyncTasks,你可以阅读我询问的问题及答案:Android:AsyncTask建议:私有类还是公共类?

在那之后我开始使用非内部和......现在我看到了很多好处.

第二个是,在Application类中保留正在运行的AsyncTask的引用- http://developer.android.com/reference/android/app/Application.html

每次启动AsyncTask时,在Application上设置它,当它完成时将其设置为null.

当一个片段/活动开始时,您可以检查是否有任何AsyncTask正在运行(通过检查它是否在应用程序上为null),然后将内部引用设置为您想要的任何内容(活动,片段等,以便您可以进行回调).

这将解决您的问题:如果您在任何确定的时间只运行1个AsyncTask,您可以添加一个简单的引用:

AsyncTask<?,?,?> asyncTask = null;
Run Code Online (Sandbox Code Playgroud)

另外,在Aplication中有一个HashMap,引用它们.

进度对话框可以遵循完全相同的原则.

  • 我同意只要你将AsyncTask的生命周期绑定到它的Parent(通过将AsyncTask定义为Activity/Fragment的内部类),很难让你的AsyncTask从其父生命周期娱乐中逃脱,但是,我不喜欢你的解决方案,看起来真是太烂了. (2认同)

归档时间:

查看次数:

55562 次

最近记录:

8 年,3 月 前