【Translation】Use Fragment to solve the problem that the state cannot be maintained when the screen rotates (the state changes)

This post addresses a frequently asked question on StackOverflow.

What is the best way to save live objects when configuration changes, such as running threads, Sockets, AsyncTask .

To answer this question, let's discuss some common difficulties developers encounter when using long-lived background tasks in the Activity lifecycle. Then, we'll cover two common, but bad, ways to fix the problem. Finally, we will illustrate the recommended solution with a sample code that uses retained fragments to achieve our goal.

Configuration Changes & Background Tasks

Configuration changes and destruction and re-creation traverse the entire Activity life cycle, and raise the issue that these events are unpredictable and may fire at any time. Concurrent background threads only exacerbate the problem. Suppose an AsyncTask is started in the Activity, and then the user immediately rotates the screen, which will cause the Activity to be destroyed and recreated. When the AsyncTask finally completes its task, it feeds the result back to the old Activity instance, completely unaware that the new activity has been created. It doesn't seem like this is a problem, the new Activity instance again wastes precious resources restarting a background thread, not knowing that the old AsyncTask is already running. For these reasons, we need to correctly and efficiently save the activity object in the Activity instance when the configuration changes.

Bad practice: save the entire Activity

Probably the most effective and often abused workaround is to disable the default destroy and recreate behavior by setting the android:configChanges attribute in the Android manifest. This simple approach makes it attractive to developers; however, Google engineers advise against it. The main concern is: Once configured, you are required to manually handle device configuration changes in your code. Handling configuration changes requires you to do a lot of extra processing to make sure every string, layout, drawing, size, etc. is consistent with the current device's configuration. If you're not careful, your application may have a bunch of bugs related to resource customization.

Another reason Google discourages its use is that many developers mistakenly believe that setting android:configChanges = "orientation" (this is just an example), will magically prevent their Activity from being destroyed and recreated in unpredictable scenarios . Not so. Configuration changes may occur for a variety of reasons, not just horizontal and vertical screen changes. Displaying your phone's content on the display, changing the default language, and modifying the device's default font scaling are all three simple examples that can trigger a device configuration change. These events will signal the system to destroy and recreate all running activities when they next resume. So setting the android:configChanges attribute is generally not a good practice.

Deprecated method: Override onRetainNonConfigurationInstance()

在Honeycomb发布前,跨越Activity实例传递活动对象的推荐方法是重写onRetainNonConfigurationInstance()getLastNonConfigurationInstance()方法。使用这种方法,传递跨越Activity 实例的活动对象仅仅需要在onRetainNonConfigurationInstance()将活动对象返回,然后在getLastNonConfigurationInstance()中取出。截止API 13,这些方法都已经被弃用,以支持更有效的Fragment的setRetainInstance(boolean)方法。它提供了一个更简洁,更模块化的方式在配置变化的时候保存对象。我们将在下一节讨论以Fragment为基础的方法。

推荐的方法:在Retained Fragment中管理对象

自从Android3.0推出Fragment。跨越Activity保留活动对象的推荐方法是在一个Retained Fragment中包装和管理它们。默认情况下,但配置发生变化时,Fragment会随着它们的宿主Activity被创建和销毁。调用Fragment#setRetaininstance(true)允许我们跳过销毁和重新创建的周期。指示系统保留当前的fragment实例,即使是在Activity被创新创建的时候。不难想到使用fragment持有像运行中的线程、AsyncTask、Socket等对象将有效地解决上面的问题。

下面代码演示如何使用fragment在配置发生变化的时候保存AsyncTask的状态。这段代码保证了最新的进度和结果能够被传回更当前正在显示的Activity实例,并确保我们不会在配置发生变化的时候丢失AsyncTask的状态。下面代码包含两个类,一个MainActivity...

复制代码
 1 /**
 2  * 这个Activity主要用来展示UI,创建一个TaskFragment来管理任务,
 3  * 从TaskFragment接收进度以及执行结果.
 4  */
 5 public class MainActivity extends Activity implements TaskFragment.TaskCallbacks {
 6 
 7   private static final String TAG_TASK_FRAGMENT = "task_fragment";
 8 
 9   private TaskFragment mTaskFragment;
10 
11   @Override
12   protected void onCreate(Bundle savedInstanceState) {
13     super.onCreate(savedInstanceState);
14     setContentView(R.layout.main);
15 
16     FragmentManager fm = getFragmentManager();
17     mTaskFragment = (TaskFragment) fm.findFragmentByTag(TAG_TASK_FRAGMENT);
18 
19     //如果Fragment不为null,那么它就是在配置变化的时候被保存下来的
20     if (mTaskFragment == null) {
21       mTaskFragment = new TaskFragment();
22       fm.beginTransaction().add(mTaskFragment, TAG_TASK_FRAGMENT).commit();
23     }
24 
25     // TODO: 初始化View, 还原保存的状态, 等等.
26   }
27 
28   //下面四个方法将在进度需要更新或者返回结果的时候被调用。
29   //MainActivity需要更新UI来反应这些变化。
30   @Override
31   public void onPreExecute() { ... }
32 
33   @Override
34   public void onProgressUpdate(int percent) { ... }
35 
36   @Override
37   public void onCancelled() { ... }
38 
39   @Override
40   public void onPostExecute() { ... }
41 }
复制代码

...和一个 TaskFragment...

复制代码
  1 /**
  2  * 这个Fragment管理一个后台任务,在状态发生变化的时候能够保存下来,不被销毁
  3  */
  4 public class TaskFragment extends Fragment {
  5 
  6   /**
  7    * 让Fragment通知Activity任务进度和返回结果的回调接口
  8    */
  9   static interface TaskCallbacks {
 10     void onPreExecute();
 11     void onProgressUpdate(int percent);
 12     void onCancelled();
 13     void onPostExecute();
 14   }
 15 
 16   private TaskCallbacks mCallbacks;
 17   private DummyTask mTask;
 18 
 19   /**
 20    * 持有一个父Activity的引用,以便在任务进度变化和需要返回结果的时候通知它。
 21    * 在每一次配置变化后,Android Framework会将新创建的Activity的引用传递给我们
 22    */
 23   @Override
 24   public void onAttach(Activity activity) {
 25     super.onAttach(activity);
 26     mCallbacks = (TaskCallbacks) activity;
 27   }
 28 
 29   /**
 30     *这个方法只会被调用一次,只在这个被保存Fragment第一次被创建的时候
 31    */
 32   @Override
 33   public void onCreate(Bundle savedInstanceState) {
 34     super.onCreate(savedInstanceState);
 35 
 36     //在配置变化的时候将这个fragment保存下来
 37     setRetainInstance(true);
 38     
 39 
 40     // 创建并执行后台任务
 41     mTask = new DummyTask();
 42     mTask.execute();
 43   }
 44 
 45   /**
 46    * 设置回调对象为null,防止我们意外导致Activity实例泄露(leak the Activity instance)
 47    */
 48   @Override
 49   public void onDetach() {
 50     super.onDetach();
 51     mCallbacks = null;
 52   }
 53 
 54   /**
 55    * 一个示例性的任务用来表示一些后台任务并且通过回调函数向Activity
 56    * 报告任务进度和返回结果
 57    *
 58    * 注意:我们需要在每一个方法中检查回调对象是否为null,以防它们
 59    * 在Activity或Fragment的onDestroy()执行后被调用。
 60    */
 61   private class DummyTask extends AsyncTask<Void, Integer, Void> {
 62 
 63     @Override
 64     protected void onPreExecute() {
 65       if (mCallbacks != null) {
 66         mCallbacks.onPreExecute();
 67       }
 68     }
 69 
 70     /**
 71      * 注意:我们不在后台线程的doInbackground方法中直接调用回调
 72      * 对象的方法,因为这样可能产生竞态条件
 73      */
 74     @Override
 75     protected Void doInBackground(Void... ignore) {
 76       for (int i = 0; !isCancelled() && i < 100; i++) {
 77         SystemClock.sleep(100);
 78         publishProgress(i);
 79       }
 80       return null;
 81     }
 82 
 83     @Override
 84     protected void onProgressUpdate(Integer... percent) {
 85       if (mCallbacks != null) {
 86         mCallbacks.onProgressUpdate(percent[0]);
 87       }
 88     }
 89 
 90     @Override
 91     protected void onCancelled() {
 92       if (mCallbacks != null) {
 93         mCallbacks.onCancelled();
 94       }
 95     }
 96 
 97     @Override
 98     protected void onPostExecute(Void ignore) {
 99       if (mCallbacks != null) {
100         mCallbacks.onPostExecute();
101       }
102     }
103   }
104 }
复制代码

事件流

MainActivity第一次启动的时候,它实例化并将TaskFragment添加到Activity的状态中。TaskFragment创建并执行AsyncTask并通过TaskCallBack接口将任务处理进度和结果回传给MainActivity。当配置变化时,MainActivity正常地经历它的生命周期事件(销毁、重新创建、onResume等),但是一旦新Activity实例被重新创建,那它就会被传递到onAttach(Activity)方法中,这就保证了TaskFragment会一直持有当前显示的新Activity实例的引用,即使是在配置发生变化的时候。另外值得注意的是,onPostExecute()不会在onDetach()onAttach()的之间被调用。具体的解释参考StackOverflow的回答以及我在Google+文章中给Doug Stevenson的回复(在评论中也有一些关于它的讨论)。

总结

在Activity的生命周期(涉及旧、新Activity的销毁和创建)中同步后台任务的运行状态可能非常棘手,并且配置发生变化加剧了这一麻烦。幸运的是使用一个retain fragment可以非常轻松地处理这些事件。它只要始终持有父Activity的引用,即使在父Activity被销毁和重新创建之后。

一个示例型的App演示怎么正确地使用Retained Fragment来达到我们的目的,Play Store下载地址。托管在Github上的源代码。下载它,用Eclipse执行Import,然后自己随意修改。

 

译文地址

 

本文永久链接:http://www.cnblogs.com/kissazi2/p/4116456.html

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326853563&siteId=291194637