實(shí)例探究Android開發(fā)中Fragment狀態(tài)的保存與恢復(fù)方法
我們都知道,類似 Activity, Fragment 有 onSaveInstanceState() 回調(diào)用來保存狀態(tài)。
在Fragment里面,利用onSaveInstanceState保存數(shù)據(jù),并可在onActivityCreated里面恢復(fù)數(shù)據(jù)。
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
...
if (savedInstanceState != null) {
// Restore the fragment's state here
}
}
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save the fragment's state here
}
但是,根據(jù)作者的經(jīng)驗(yàn),這個(gè)方法調(diào)用非常的不靠普。Fragment 在屏幕旋轉(zhuǎn)和返回堆棧(Backstack)中的時(shí)候,都會(huì)創(chuàng)建一個(gè)全新的 View,這個(gè) onSaveInstanceState() 方法經(jīng)常會(huì)出現(xiàn)不會(huì)被調(diào)用的情況,導(dǎo)致 Fragment 的狀態(tài)丟失。
我們來通過接下來的實(shí)例尋找解決方法。
首先,盡管已經(jīng)有了一個(gè)類似 Activity 中的 onSaveInstanceState 方法,但是它顯然不能覆蓋所有情況。換種說法就是,你不能僅僅依賴于 onSaveInstanceState 方法來保存/恢復(fù)視圖的狀態(tài)。這里有一些案例研究。
案例1:只有一個(gè) Fragment 在棧中時(shí),旋轉(zhuǎn)屏幕

屏幕旋轉(zhuǎn)是用來測(cè)試實(shí)例狀態(tài)的保存/恢復(fù)的最簡(jiǎn)單的案例。這種情況很容易處理,你僅僅需要簡(jiǎn)單地保存一些東西,比如:成員變量,它也會(huì)在屏幕旋轉(zhuǎn)時(shí)在 onSaveInstanceState 丟失,在 onActivityCreated 或者 onViewStateRestored 中恢復(fù),如下所示:
int someVar;
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt("someVar", someVar);
outState.putString(“text”, tv1.getText().toString());
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
someVar = savedInstanceState.getInt("someVar", 0);
tv1.setText(savedInstanceState.getString(“text”));
}
看上去是不是很好?不過也不是全不管用。這種情況是在 onSaveInstanceState 不被回調(diào),但是視圖重新生成。這意味著什么?UI 里的所有東西都沒了。下面就是這種案例。
案例2:后退棧(Back Stack)中的 Fragment

當(dāng)一個(gè) fragment 從后退棧中返回時(shí)(Fragment A就是在這種情況),F(xiàn)ragment A 中視圖將會(huì)遵循下圖的 Fragment 生命周期被重新創(chuàng)造出來。

你將會(huì)看到Fragment從后退棧中返回時(shí),會(huì)回調(diào) onDestroyview 方法和 onCreateview 方法。不管怎樣,顯然在這種情況 onSaveInstanceState 方法沒有被調(diào)用。結(jié)果就是 UI 里的所有都沒有了,然后默認(rèn)按照 Layout XML 文件中定義的來重新創(chuàng)建。
不管怎樣,實(shí)現(xiàn)了內(nèi)在視圖狀態(tài)保存的視圖,如:帶有 android:freeezeText 的 EditText 或者 TextView,仍然能夠保存好視圖的狀態(tài),這是因?yàn)?Fragment 實(shí)現(xiàn)了對(duì)內(nèi)在視圖的狀態(tài)保存,但我們這些開發(fā)者不能抓住這些事件。我們唯一能做的就是在 onDestroyView 方法中手動(dòng)保存實(shí)例狀態(tài)。
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save State Here
}
@Override
public void onDestroyView() {
super.onDestroyView();
// Save State Here
}
問題也隨之而來,onDestroyView 不提供任何幫助來保存實(shí)例狀態(tài)到一個(gè) Bundle,那我們應(yīng)該把這些實(shí)例狀態(tài)保存到什么地方呢? 答案就是 Argument, 它會(huì)隨著 Fragment 一直保存著。
現(xiàn)在代碼如下所示:
Bundle savedState;
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Restore State Here
if (!restoreStateFromArguments()) {
// First Time running, Initialize something here
}
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save State Here
saveStateToArguments();
}
@Override
public void onDestroyView() {
super.onDestroyView();
// Save State Here
saveStateToArguments();
}
private void saveStateToArguments() {
savedState = saveState();
if (savedState != null) {
Bundle b = getArguments();
b.putBundle(“internalSavedViewState8954201239547”, savedState);
}
}
private boolean restoreStateFromArguments() {
Bundle b = getArguments();
savedState = b.getBundle(“internalSavedViewState8954201239547”);
if (savedState != null) {
restoreState();
return true;
}
return false;
}
/////////////////////////////////
// Restore Instance State Here
/////////////////////////////////
private void restoreState() {
if (savedState != null) {
// For Example
//tv1.setText(savedState.getString(“text”));
}
}
//////////////////////////////
// Save Instance State Here
//////////////////////////////
private Bundle saveState() {
Bundle state = new Bundle();
// For Example
//state.putString(“text”, tv1.getText().toString());
return state;
}
你能夠容易地在 saveState 保存你的 fragment 的狀態(tài),在 restoreState 恢復(fù)狀態(tài)。現(xiàn)在已經(jīng)看起來好多了不少。我們已經(jīng)快結(jié)束了,但是還有一種怪異的情況。
案例3:在后退棧中超過一個(gè) fragment 時(shí),旋轉(zhuǎn)屏幕兩次

當(dāng)你旋轉(zhuǎn)屏幕一次,onSaveInstanceState 會(huì)被回調(diào),正如我們所期待的,UI 的狀態(tài)會(huì)被保存。但是當(dāng)你旋轉(zhuǎn)屏幕超過一次,上述的代碼可能導(dǎo)致應(yīng)用的崩潰。原因就是盡管當(dāng)你旋轉(zhuǎn)屏幕時(shí), onSaveInstanceState 方法被調(diào)用,但是在后退棧中的 fragment 會(huì)完全銷毀視圖,直到你瀏覽返回到原來那個(gè) fragment 才會(huì)重新創(chuàng)建。因此,你再次旋轉(zhuǎn)屏幕,就沒有視圖來保存狀態(tài)。當(dāng)你試圖訪問那些不存在的視圖,saveState() 將會(huì)導(dǎo)致 NullPointerException,從而使應(yīng)用崩潰。
方法就是檢查在 fragment 中視圖是否存在。如果存在那就保存,如果不存在,那就在 Argument 中 savedState 不需要保存,然后返回時(shí)保存?;蛘呶覀兩踔敛恍枰鋈魏问拢?yàn)樵贏rgument 中已經(jīng)做好了。
private void saveStateToArguments() {
if (getView() != null)
savedState = saveState();
if (savedState != null) {
Bundle b = getArguments();
b.putBundle(“savedState”, savedState);
}
}
哈,現(xiàn)在全都解決了!
Fragment 最終模版
如下就是我現(xiàn)在用于我工作中的 fragment 模版。
import android.os.Bundle;
import android.support.v4.app.Fragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.inthecheesefactory.thecheeselibrary.R;
/**
*Created by nuuneoi on 11/16/2014.
*/
public class StatedFragment extends Fragment {
Bundle savedState;
public StatedFragment() {
super();
}
@Override
public void onActivityCreated(Bundle savedInstanceState) {
super.onActivityCreated(savedInstanceState);
// Restore State Here
if (!restoreStateFromArguments()) {
// First Time, Initialize something here
onFirstTimeLaunched();
}
}
protected void onFirstTimeLaunched() {
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save State Here
saveStateToArguments();
}
@Override
public void onDestroyView() {
super.onDestroyView();
// Save State Here
saveStateToArguments();
}
////////////////////
// Don't Touch !!
////////////////////
private void saveStateToArguments() {
if (getView() != null)
savedState = saveState();
if (savedState != null) {
Bundle b = getArguments();
b.putBundle("internalSavedViewState8954201239547", savedState);
}
}
////////////////////
// Don't Touch !!
////////////////////
private boolean restoreStateFromArguments() {
Bundle b = getArguments();
savedState = b.getBundle("internalSavedViewState8954201239547");
if (savedState != null) {
restoreState();
return true;
}
return false;
}
/////////////////////////////////
// Restore Instance State Here
/////////////////////////////////
private void restoreState() {
if (savedState != null) {
// For Example
//tv1.setText(savedState.getString("text"));
onRestoreState(savedState);
}
}
protected void onRestoreState(Bundle savedInstanceState) {
}
//////////////////////////////
// Save Instance State Here
//////////////////////////////
private Bundle saveState() {
Bundle state = new Bundle();
// For Example
//state.putString("text", tv1.getText().toString());
onSaveState(state);
return state;
}
protected void onSaveState(Bundle outState) {
}
}
如果你使用這個(gè)模版,僅僅只需簡(jiǎn)單地繼承這個(gè)類StatedFragment,然后在 onSaveState() 中保存事物,在 onRestoreState() 中恢復(fù)它們。上述代碼就會(huì)為你做好剩下的工作,我相信這已經(jīng)覆蓋了我已知的可能情況。
setRetainInstance 能夠幫助開發(fā)者在布局改變時(shí)(如:屏幕旋轉(zhuǎn))處理成員變量 而你可能注意到我沒有設(shè)置 setRetaionInstance 為 true。請(qǐng)記住,這就是我的目的,因?yàn)閟etRetainInstance(true)并沒有覆蓋全部的情況。最主要的原因是你不能一次又一次地保存那些經(jīng)常背使用的嵌套 fragment 。所以我建議就是不要保存實(shí)例,除非你100%確定這個(gè) fragment 不會(huì)用于嵌套。
用法:
好消息。這個(gè)博客講述的 StateFragment 現(xiàn)在加入了一個(gè)非常容易使用的庫,現(xiàn)在已經(jīng)在 jcenter 上發(fā)布了?,F(xiàn)在你可以簡(jiǎn)單地在你工程的 build.gradle 文件中加上一個(gè)依賴。如下所示:
dependencies {
compile 'com.inthecheesefactory.thecheeselibrary:stated-fragment-support-v4:0.9.1'
}
繼承 StateFragment ,然后在 onSaveState(Bundle outState) 中保存狀態(tài),在 onRestoreState(Bundle saveInstanceState)中恢復(fù)狀態(tài)。如果你想在 fragment 第一次啟動(dòng)時(shí)做點(diǎn)什么的話,你也可以覆蓋 onFirstTimeLaunched() 方法(在之后不會(huì)被調(diào)用)。
public class MainFragment extends StatedFragment {
...
/**
* Save Fragment's State here
*/
@Override
protected void onSaveState(Bundle outState) {
super.onSaveState(outState);
// For example:
//outState.putString("text", tvSample.getText().toString());
}
/**
* Restore Fragment's State here
*/
@Override
protected void onRestoreState(Bundle savedInstanceState) {
super.onRestoreState(savedInstanceState);
// For example:
//tvSample.setText(savedInstanceState.getString("text"));
}
...
}
End
最后,不要忽略狀態(tài)保存的問題,在內(nèi)存不足或者系統(tǒng)限制比較苛刻的機(jī)器上面,都有可能出現(xiàn)Fragment或Activity被回收,比如經(jīng)常出現(xiàn)拍照之后返回App,但App里面的數(shù)據(jù)被清空了,更常見的就是旋轉(zhuǎn)屏幕,所以要保存好自己需要的數(shù)據(jù)。
- android中Activity詳解(生命周期、以各種方式啟動(dòng)Activity、狀態(tài)保存,完全退出等)
- Android保存Activity狀態(tài)的方法
- Android學(xué)習(xí)小結(jié)之Activity保存和恢復(fù)狀態(tài)
- Android 保存Fragment 切換狀態(tài)實(shí)例代碼
- Android中用onSaveInstanceState保存Fragment狀態(tài)的方法
- android初學(xué)者必須掌握的Activity狀態(tài)的四大知識(shí)點(diǎn)(必讀)
- Android編程實(shí)現(xiàn)隱藏狀態(tài)欄及測(cè)試Activity是否活動(dòng)的方法
- Android中檢查、監(jiān)聽電量和充電狀態(tài)的方法
- Android中監(jiān)聽軟鍵盤顯示狀態(tài)實(shí)現(xiàn)代碼
- Android編程實(shí)現(xiàn)狀態(tài)保存的方法分析
相關(guān)文章
Android開發(fā)實(shí)現(xiàn)NFC刷卡讀取的兩種方式
這篇文章主要為大家詳細(xì)介紹了Android開發(fā)中實(shí)現(xiàn)NFC刷卡讀取的兩種方式,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09
Kotlin類與屬性及構(gòu)造函數(shù)的使用詳解
這篇文章主要介紹了Kotlin語言中類與屬性及構(gòu)造函數(shù)的使用,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-09-09
Android性能優(yōu)化以及數(shù)據(jù)優(yōu)化方法
我和大家之前一起探討了在Android中對(duì)SQLite數(shù)據(jù)庫的操作優(yōu)化細(xì)節(jié)。今天談?wù)凙ndroid性能數(shù)據(jù)優(yōu)化方法,需要的朋友可以參考下2016-05-05
Android可簽到日歷控件的實(shí)現(xiàn)方法
這篇文章主要為大家詳細(xì)介紹了Android可簽到日歷控件的實(shí)現(xiàn)方法,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-05-05
Flutter質(zhì)感設(shè)計(jì)之進(jìn)度條
這篇文章主要為大家詳細(xì)介紹了Flutter質(zhì)感設(shè)計(jì)之進(jìn)度條,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-08-08
Android動(dòng)態(tài)自定義圓形進(jìn)度條
這篇文章主要介紹了Android動(dòng)態(tài)自定義圓形進(jìn)度條,需要的朋友可以參考下2017-03-03
Android獲取驗(yàn)證碼倒計(jì)時(shí)實(shí)現(xiàn)代碼
這篇文章主要為大家詳細(xì)介紹了Android獲取驗(yàn)證碼倒計(jì)時(shí)的實(shí)現(xiàn)代碼,具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-07-07

