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

亦楓發表於2016-12-26

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

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

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

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

無論如何,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 屬性定義的。

讓我們通過視覺化動畫效果看一下:

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

[譯]Android Activity 和 Fragment 狀態儲存與恢復的最佳實踐
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 那樣發生所有事情:

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

[譯]Android Activity 和 Fragment 狀態儲存與恢復的最佳實踐
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 將會被銷燬和重建。

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

這種情況屬於,Fragment 沒有被銷燬,但 Fragment 的 View 被銷燬。因此,沒有發生 Instance 狀態儲存。那麼那些通過 Fragment 生命週期重新建立的 View 發生了什麼呢?

不是問題。Android 是這麼設計的。在這種情況下,View 狀態儲存和恢復在 Fragment 內部被呼叫。因此,每一個內部實現 View 類儲存和恢復方法的 View,例如 EditText 或者 TextView,只要設定了 android:freezeText="true",都將被自動儲存和恢復狀態。資料和 View 的對應呈現關係和上面一樣。

[譯]Android Activity 和 Fragment 狀態儲存與恢復的最佳實踐
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),歡迎關注。

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

相關文章