美文网首页
[翻译]使用Fragment处理配置更改(Handling Co

[翻译]使用Fragment处理配置更改(Handling Co

作者: 豆沙包67 | 来源:发表于2015-08-12 15:29 被阅读509次

    原文地址

    StackOverflow上这类问题很常见

    What is the best way to retain active objects—such as runningThreads、Sockets、 andAsyncTasks—across device configuration changes?

    回答问题之前,我们先讨论开发者通常在处理与Activity生命周期相关的耗时任务会遇到的困难,接着,我们会讨论两种常见解决方法的缺陷,最后,我们会使用持久化Framgnet作为实例代码,给出值得推荐解决方案。

    屏幕旋转 & 后台任务

    屏幕旋转时,Activity必须经历生命周期的重构,而事件的发生却是不可预测的。后台并发任务的处理无异加剧了这个难题。

    比如,Activity启动了AsyncTask之后,用户旋转手机屏幕,导致Activity被销毁和重构。AsyncTask完成任务后,并不知道存在新Activity,错误地把结果转交给旧Activity。另一方面,新Activity并不知道AsyncTask的存在和处理结果,会重新启动AsyncTask,导致资源浪费。因此,在屏幕旋转的过程中,正确有效地保存Activity信息就显得尤为重要。

    坏方法:固定Activity的方向

    世界上最取巧,最被滥用的方法就是通过固定Activity方向,阻止Activity的重构。
    在AndroidManifest.xml文件中设置android:configChanges
    这个简单的方法非常吸引开发者。谷歌工程师并不推荐这种做法。

    首当其冲需要使用代码处理屏幕旋转,意味着花更多的精力确保每个字符串(string),布局(layout),尺寸(dimen)等与当前屏幕方向保持同步,处理不当很容易会造成一系列的资源特定bug。

    谷歌另一个不鼓励使用该方法的原因,很多开发者错误地设置android:configChanges="orientation"(举例)会意外地阻止底层Activity摧毁和重构。不单止屏幕旋转,还有各种各样的原因会导致配置改变,把设备接到显示器上、改变默认语言、改变默认字体大小只是三个会改变配置的触发事件。所以,设置android:configChanges并不是一个好方法。

    过时,重写onRetainNonConfigurationInstance()

    在Android Honeycomb(Android 3.1系统,译者注)版本之前,推荐重写onRetainNonConfigurationInstance()getLastNonConfigurationInstance()在多个Activity实例间转移对象。onRetainNonConfigurationInstance()用于传递对象而getLastNonConfigurationInstance()用于获取对象。在API 13(Android 3.2系统,译者注)这些方法过时,支持使用更方便的模块化方法Fragment中setRetainInstance(boolean)来保存对象。下一章节我们会讨论这种方法。

    推荐:在持久化Fragment中管理对象

    从Android 3.0开始引入Fragment的概念,在Activity中持久化对象的方法,是通过持久化Fragment包装和管理这些对象。默认情况下,在配置发生改变时Fragment的重构是跟随父Activity的。通过调用Fragment#setRetainInstance(true),跳过销毁重构的过程,告诉系统在Acitivity重构时保持当前Fragment实例的状态。这在我们运行Thread,AsyncTask,Socket,使用持久化Fragment就变得相当有利。

    下面的样例代码示范,在配置改变的情况下,怎么去使用持久化Fragment来保存AsyncTask。代码保证了进度更新和正确传递结果到Activity,在配置改变时不会泄露AsyncTask的引用。
    代码包括两个类,第一个是MainActivity

     * This Activity displays the screen's UI, creates a TaskFragment
     * to manage the task, and receives progress updates and results 
     * from the TaskFragment when they occur.
     */
    public class MainActivity extends Activity implements TaskFragment.TaskCallbacks {
    
      private static final String TAG_TASK_FRAGMENT = "task_fragment";
    
      private TaskFragment mTaskFragment;
    
      @Override
      protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main);
    
        FragmentManager fm = getFragmentManager();
        mTaskFragment = (TaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);
    
        // If the Fragment is non-null, then it is currently being
        // retained across a configuration change.
        if (mTaskFragment == null) {
          mTaskFragment = new TaskFragment();
          fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();
        }
    
        // TODO: initialize views, restore saved state, etc.
      }
    
      // The four methods below are called by the TaskFragment when new
      // progress updates or results are available. The MainActivity 
      // should respond by updating its UI to indicate the change.
    
      @Override
      public void onPreExecute() { ... }
    
      @Override
      public void onProgressUpdate(int percent) { ... }
    
      @Override
      public void onCancelled() { ... }
    
      @Override
      public void onPostExecute() { ... }
    }
    

    还有TaskFragment

     * This Fragment manages a single background task and retains 
     * itself across configuration changes.
     */
    public class TaskFragment extends Fragment {
    
      /**
       * Callback interface through which the fragment will report the
       * task's progress and results back to the Activity.
       */
      interface TaskCallbacks {
        void onPreExecute();
        void onProgressUpdate(int percent);
        void onCancelled();
        void onPostExecute();
      }
    
      private TaskCallbacks mCallbacks;
      private DummyTask mTask;
    
      /**
       * Hold a reference to the parent Activity so we can report the
       * task's current progress and results. The Android framework 
       * will pass us a reference to the newly created Activity after 
       * each configuration change.
       */
      @Override
      public void onAttach(Activity activity) {
        super.onAttach(activity);
        mCallbacks = (TaskCallbacks) activity;
      }
    
      /**
       * This method will only be called once when the retained
       * Fragment is first created.
       */
      @Override
      public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
    
        // Retain this fragment across configuration changes.
        setRetainInstance(true);
    
        // Create and execute the background task.
        mTask = new DummyTask();
        mTask.execute();
      }
    
      /**
       * Set the callback to null so we don't accidentally leak the 
       * Activity instance.
       */
      @Override
      public void onDetach() {
        super.onDetach();
        mCallbacks = null;
      }
    
      /**
       * A dummy task that performs some (dumb) background work and
       * proxies progress updates and results back to the Activity.
       *
       * Note that we need to check if the callbacks are null in each
       * method in case they are invoked after the Activity's and
       * Fragment's onDestroy() method have been called.
       */
      private class DummyTask extends AsyncTask<Void, Integer, Void> {
    
        @Override
        protected void onPreExecute() {
          if (mCallbacks != null) {
            mCallbacks.onPreExecute();
          }
        }
    
        /**
         * Note that we do NOT call the callback object's methods
         * directly from the background thread, as this could result 
         * in a race condition.
         */
        @Override
        protected Void doInBackground(Void... ignore) {
          for (int i = 0; !isCancelled() && i < 100; i++) {
            SystemClock.sleep(100);
            publishProgress(i);
          }
          return null;
        }
    
        @Override
        protected void onProgressUpdate(Integer... percent) {
          if (mCallbacks != null) {
            mCallbacks.onProgressUpdate(percent[0]);
          }
        }
    
        @Override
        protected void onCancelled() {
          if (mCallbacks != null) {
            mCallbacks.onCancelled();
          }
        }
    
        @Override
        protected void onPostExecute(Void ignore) {
          if (mCallbacks != null) {
            mCallbacks.onPostExecute();
          }
        }
      }
    }
    

    事件流

    MainActivity第一次启动时,实例化同时添加TaskFragment到Activity。TaskFragment创建并执行AsyncTask,将更新结果传递回MainActivity通过TaskCallbacks接口。

    当配置发生改变时,MainActivity正常走生命周期的重构方法,一旦新的Activity创建成功后会回调Fragmentd的onAttach(Activity)方法,即使在配置改变的情况下,保证Fragment当前持有的是最新的Activity的引用。

    代码运行的结果是简单且可靠的;应用程序框架会处理Activity重建后的实例,TaskFragmentAsyncTask无需关注配置的改变。onPostExecute()可以在onDetach()onAttach()方法回调之间执行。
    参考在StackOverFlow上的回答和在Google+回答Doug Stevenson的问题。

    结论

    与Activity生命周期相关的同步后台任务的处理是很有技巧的,配置改变也容易令人迷惑。幸运的是,通过长期持有父Activity的引用,即使在被重构的情况下,持久化Fragment使得这些事件的处理变得简单。
    你可以在Play Store上下载到代码,源码在github上开源了,下载,import到Eclipse,随心所欲地改吧;)

    Demo视图

    译者注

    屏幕旋转总结

    • 不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏时会执行一次,切竖屏时会执行两次
    • 设置Activity的android:configChanges="orientation"时,切屏还是会重新调用各个生命周期,切横、竖屏时只会执行一次
    • 设置Activity的android:configChanges="orientation|keyboardHidden"时,切屏不会重新调用各个生命周期,只会执行onConfigurationChanged方法

    意见修改

    • 欢迎指出翻译有误的地方

    相关文章

      网友评论

          本文标题:[翻译]使用Fragment处理配置更改(Handling Co

          本文链接:https://www.haomeiwen.com/subject/trwiqttx.html