譯者亦楓注:對于 Activity、Fragment 和 View 是如何保存與恢復狀態的問題,相信很多開發人員都處于一知半解的狀態。最近剛好在總結 Fragment 的使用注意事項,無意中從網上看到國外的一篇好文,對這個問題做了一個全面的解析。加之使用可視化的動畫效果,使我們理解起來更加輕松。拜讀過后,豁然開朗,同時不得不感慨,國外作者對于知識通透的理解能力和寫作清晰的表達能力。然后,然后就一定要翻譯過來,加以學習并保存記錄之。
作者:「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
屬性定義的。
讓我們通過可視化動畫效果看一下:
這就是為什么輸入在 EditText 中的文本內容在 Activity 已經被銷毀同時我們不用做任何事情的情況下依然能夠保存的原因。這沒什么不可思議的。這些 View 的狀態會自動被收集和恢復回來。
同時這也是為什么那些沒有定義 android:id
屬性的 View 不能恢復狀態的原因。
雖然這些 View 的狀態可以被自動保存,但是 Activity 成員變量卻不行。他們將隨著 Activity 一起被銷毀。你不得不通過 onSaveInstanceState
和 onRestoreInstanceState
方法手動保存和恢復這些成員變量。
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 那樣發生所有事情:
也意味著每一個成員變量也被銷毀。你不得不通過 onSaveInstanceState
和 onRestoreInstanceState
方法分別手動保存和恢復這些成員變量。但請注意,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 的對應呈現關系和上面一樣。
需要注意的是在這種情況下只有 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 提供了一個通過 onSaveInstanceState
和 onRestoreInstanceState
方法用于 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
、TextView
、Checkbox
等,都在內部實現了這些事情。而你所需要做的就是開啟這個功能,比如你必須設置TextView
的 android:freezeText
屬性值為 true 來使用這個功能。
但是如果是來自網上的第三方庫里面的自定義 View 呢?我不得不說他們中的很多都沒有實現這部分代碼而導致我們在實際使用過程中出現很大的問題。
如果你決定使用第三方自定義 View,你必須保證這些 View 內部已經實現 View 狀態保存和恢復,否則你必須創建一個子類繼承自這些 View 并且自己實現 onSaveInstanceState
和 onRestoreInstanceState
方法。
//
// 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 恢復狀態的方式。另外對于之前文章造成的困惑表示歉意。>_<