[翻譯]使用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,隨心所欲地改吧;)

譯者注

屏幕旋轉總結

  • 不設置Activity的android:configChanges時,切屏會重新調用各個生命週期,切橫屏時會執行一次,切豎屏時會執行兩次
  • 設置Activity的android:configChanges=”orientation”時,切屏還是會重新調用各個生命週期,切橫、豎屏時只會執行一次
  • 設置Activity的android:configChanges=”orientation|keyboardHidden”時,切屏不會重新調用各個生命週期,只會執行onConfigurationChanged方法

意見修改

  • 歡迎指出翻譯有誤的地方
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章