[翻译]使用Fragment处理配置更改(Handling Configuration Changes With Fragments)

原文地址

在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 {

    @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,随心所欲地改吧;)

[翻译]使用Fragment处理配置更改(Handling Configuration Changes With Fragments)_第1张图片
Demo视图

译者注

屏幕旋转总结

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

意见修改

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

你可能感兴趣的:([翻译]使用Fragment处理配置更改(Handling Configuration Changes With Fragments))