[譯]Android Activity 和 Fragment 狀態保存與恢復的最佳實踐

譯者亦楓注:對於 Activity、Fragment 和 View 是如何保存與恢復狀態的問題,相信不少開發人員都處於只知其一;不知其二的狀態。最近恰好在總結 Fragment 的使用注意事項,無心中從網上看到國外的一篇好文,對這個問題作了一個全面的解析。加之使用可視化的動畫效果,使咱們理解起來更加輕鬆。拜讀事後,豁然開朗,同時不得不感慨,國外做者對於知識通透的理解能力和寫做清晰的表達能力。而後,而後就必定要翻譯過來,加以學習並保存記錄之。java

原文:The Real Best Practices to Save/Restore Activity's and Fragment's state. (StatedFragment is now deprecated)android

做者:「nuuneoi」,一名擁有六年安卓應用程序開發經驗和超過十二年手機端應用開發行業經驗的全棧工程師。git

幾個月前我發表了一篇有關 Fragment 狀態保存和恢復的文章:多是目前爲止保存和恢復 Fragment 狀態的最佳方式(亦楓注:該文章已被刪除,但 GitHub 上依然保有代碼實現,可參考 StatedFragment。另外,我發現中外做者在標題設定上怎麼套路都是一致的 ^_^)。這篇文章收到了來自世界各地安卓開發人員的較有價值的反饋。很是感謝大家 =)github

不管如何,StatedFragment 打破了常規設計模式,以一種不一樣的方式實現,就像 Android 設計 Fragment 之初就假定可以讓安卓開發人員更容易理解 Fragment 的狀態保存和恢復,如同 Activity 的作法同樣(View 狀態與 Instance 狀態同時變遷)。因此我作了一個實驗,開發出 StatedFragment 並看看到底能發展成怎樣。是否更容易理解?這種模式是否更加利於開發?設計模式

此刻,經歷了兩個月的實踐,我想我已經獲得告終果。儘管 StatedFragment 理解起來稍微容易一些,但仍是遇到了一個大問題。StatedFragment 打破了 Android View 架構的設計模式,因此我想這會致使一個長久的負面問題。事實上,我已經開始感受到個人代碼有些怪怪的了......微信

出於這個緣由,我決定從如今開始廢棄 StatedFragment。同時爲了對這個錯誤的出現表示歉意,我寫下這篇博文,向大家展現如何用 Android 的設計方式保存和恢復 Fragment 狀態的最佳實踐。架構

理解 Activity 狀態保存和恢復時發生了什麼


當 Activity 的 onSaveInstanceState 方法被調用時,Activity 會自動收集 View Hierachy(視圖層次)中每個 View 的狀態。請注意,只有內部實現了 View 類狀態保存和恢復方法的控件才能被收集狀態數據。一旦 onRestoreInstanceState 方法被調用,Activity 將這些收集的數據回傳給 View Hierachy 中的 View,而這種回傳時數據與 View 一一對應關係的依據就是 View 提供以前保存數據時的相同 id,一般在佈局中經過 android:id 屬性定義的。app

讓咱們經過可視化動畫效果看一下:ide

Activity State Saving

Activity State Restoring

這就是爲何輸入在 EditText 中的文本內容在 Activity 已經被銷燬同時咱們不用作任何事情的狀況下依然可以保存的緣由。這沒什麼難以想象的。這些 View 的狀態會自動被收集和恢復回來。佈局

同時這也是爲何那些沒有定義 android:id 屬性的 View 不能恢復狀態的緣由。

雖然這些 View 的狀態能夠被自動保存,可是 Activity 成員變量卻不行。他們將隨着 Activity 一塊兒被銷燬。你不得不經過 onSaveInstanceStateonRestoreInstanceState 方法手動保存和恢復這些成員變量。

public class MainActivity extends AppCompatActivity {

    // These variable are destroyed along with Activity
    private int someVarA;
    private String someVarB;

    ...

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("someVarA", someVarA);
        outState.putString("someVarB", someVarB);
    }

    @Override
    protected void onRestoreInstanceState(Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        someVarA = savedInstanceState.getInt("someVarA");
        someVarB = savedInstanceState.getString("someVarB");
    }

}複製代碼

這就是恢復 Activity Instance 狀態和 View 狀態你所須要作的事情。

Fragment 狀態保存和恢復時發生了什麼


假設 Fragment 被系統銷燬,就會像 Activity 那樣發生全部事情:

Fragment State Saving

Fragment State Restoring

也意味着每個成員變量也被銷燬。你不得不經過 onSaveInstanceStateonRestoreInstanceState 方法分別手動保存和恢復這些成員變量。但請注意,Fragment 類裏面沒有 onRestoreInstanceState 方法:

public class MainFragment extends Fragment {

    // These variable are destroyed along with Activity
    private int someVarA;
    private String someVarB;

    ...

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putInt("someVarA", someVarA);
        outState.putString("someVarB", someVarB);
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        someVarA = savedInstanceState.getInt("someVarA");
        someVarB = savedInstanceState.getString("someVarB");
    }

}複製代碼

對於 Fragment,我認爲你須要知道一些與 Activity 不一樣的地方。一旦 Fragment 從回退棧(BackStack)中返回時,View 將會被銷燬和重建。

這種狀況屬於,Fragment 沒有被銷燬,但 Fragment 的 View 被銷燬。所以,沒有發生 Instance 狀態保存。那麼那些經過 Fragment 生命週期從新建立的 View 發生了什麼呢?

不是問題。Android 是這麼設計的。在這種狀況下,View 狀態保存和恢復在 Fragment 內部被調用。所以,每個內部實現 View 類保存和恢復方法的 View,例如 EditText 或者 TextView,只要設置了 android:freezeText="true",都將被自動保存和恢復狀態。數據和 View 的對應呈現關係和上面同樣。

Fragment From BackStack

須要注意的是在這種狀況下只有 View 被銷燬和重建。Fragment 實例仍然在那兒,包括實例裏的成員變量。因此你不須要對成員變量作任何事情。不須要額外添加任何代碼:

public class MainFragment extends Fragment {

    // These variable still persist in this case
    private int someVarA;
    private String someVarB;

    ...

}複製代碼

你可能已經注意到,若是 Fragment 中使用到的每個 View 內部都實現了 View 類恢復和保存的方法,在這種狀況下你就不須要作任何事情,由於 View 狀態會自動恢復而且 Fragment 中的成員變量也仍然存在。

因此,有關 Fragment 狀態保存和恢復最佳實踐的第一個條件是...

你項目中用到的每個 View 內部必須實現狀態保存和恢復方法


Android 提供了一個經過 onSaveInstanceStateonRestoreInstanceState方法用於 View 內部保存和恢復狀態的機制。開發人員在自定義 View 時實現這兩個方法便可:

public class CustomView extends View {

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        // Save current View's state here
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        // Restore View's state here
    }

    ...

}複製代碼

基本上每個單獨的標準的 View 控件,如 EditTextTextViewCheckbox 等,都在內部實現了這些事情。而你所須要作的就是開啓這個功能,好比你必須設置TextViewandroid:freezeText 屬性值爲 true 來使用這個功能。

可是若是是來自網上的第三方庫裏面的自定義 View 呢?我不得不說他們中的不少都沒有實現這部分代碼而致使咱們在實際使用過程當中出現很大的問題。

若是你決定使用第三方自定義 View,你必須保證這些 View 內部已經實現 View 狀態保存和恢復,不然你必須建立一個子類繼承自這些 View 而且本身實現 onSaveInstanceStateonRestoreInstanceState 方法。

//
// Assumes that SomeSmartButton is a 3rd Party view that
// View State Saving/Restoring are not implemented internally
//
public class SomeBetterSmartButton extends SomeSmartButton {

    ...

    @Override
    public Parcelable onSaveInstanceState() {
        Bundle bundle = new Bundle();
        // Save current View's state here
        return bundle;
    }

    @Override
    public void onRestoreInstanceState(Parcelable state) {
        super.onRestoreInstanceState(state);
        // Restore View's state here
    }

    ...

}複製代碼

固然若是你建立了本身的自定義 View 或者自定義 ViewGroup ,不要忘了也要實現這兩個方法。必定要記住項目中用到的每一種類型的 View 都要實現這部分代碼。

同時也不要忘記分配 android:id 屬性給 Layout 佈局中你須要支持狀態保存和恢復的每個 View,不然這些 View 根本不會支持恢復狀態。

<EditText android:id="@+id/editText1" android:layout_width="match_parent" android:layout_height="wrap_content" />

    <EditText android:id="@+id/editText2" android:layout_width="match_parent" android:layout_height="wrap_content" />

    <CheckBox android:id="@+id/cbAgree" android:text="I agree" android:layout_width="wrap_content" android:layout_height="wrap_content" />複製代碼

到這裏咱們只進行到一半!

明確區分 Fragment 狀態和 View 狀態


爲了使你的代碼變得更加清晰和易於維護,你必須將 Fragment 狀態和 View 狀態區分開來。對於任何屬於 View 的屬性,在 View 內部實現狀態保存和恢復。而對於那些屬於 Fragment 的屬性,就在 Fragment 內部實現便可。舉個例子:

public class MainFragment extends Fragment {

    ...

    private String dataGotFromServer;

    @Override
    public void onSaveInstanceState(Bundle outState) {
        super.onSaveInstanceState(outState);
        outState.putString("dataGotFromServer", dataGotFromServer);
    }

    @Override
    public void onActivityCreated(Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        dataGotFromServer = savedInstanceState.getString("dataGotFromServer");
    }

    ...

}複製代碼

我再重複一遍,不要在 Fragment 的 onSaveInstanceState 方法中保存 View 狀態,反之亦然。

StatedFragment


請按上面說起的方式保存和恢復 Activity、Fragment 和 View 的狀態。如今讓我將 StatedFragment 標記廢除。

然而 StatedFragment 在嵌套 Fragment 中獲取 onActivityResult 的功能使用起來仍然不錯。爲了不未來產生疑惑,我決定從 v0.10.0 版本開始將這個功能單獨拆分到一個新的命名爲 NestedActivityResultFragment 的類中。

有關它的更多信息都在網址 github.com/nuuneoi/Sta…,請隨時自由查閱。

但願這篇博文中的可視化動畫可以幫助你清晰地理解 Activity 、Fragment 和 View 恢復狀態的方式。另外對於以前文章形成的困惑表示歉意。>_<

歡迎關注我


本文由 亦楓 創做並首發於 亦楓的我的博客 ,同步推送我的微信公衆號:技術鳥(NiaoTech),歡迎關注。

相關文章
相關標籤/搜索