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

譯者亦楓注:對于 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 屬性定義的。

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

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 控件,如 EditText、TextViewCheckbox 等,都在內部實現了這些事情。而你所需要做的就是開啟這個功能,比如你必須設置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 的狀態?,F在讓我將 StatedFragment 標記廢除。

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

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

希望這篇博文中的可視化動畫能夠幫助你清晰地理解 Activity 、Fragment 和 View 恢復狀態的方式。另外對于之前文章造成的困惑表示歉意。>_<

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,578評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,701評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,691評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,974評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,694評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,026評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,015評論 3 450
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,193評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,719評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,442評論 3 360
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,668評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,151評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,846評論 3 351
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,255評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,592評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,394評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,635評論 2 380

推薦閱讀更多精彩內容