[翻译]使用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<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方法

意见修改

  • 欢迎指出翻译有误的地方
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,098评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,213评论 2 380
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,960评论 0 336
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,519评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,512评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,533评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,914评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,574评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,804评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,563评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,644评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,350评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,933评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,908评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,146评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,847评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,361评论 2 342

推荐阅读更多精彩内容