編輯:關於Android編程
我們都知道,類似 Activity, Fragment 有 onSaveInstanceState() 回調用來保存狀態。
在Fragment裡面,利用onSaveInstanceState保存數據,並可在onActivityCreated裡面恢復數據。
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
}
但是,根據作者的經驗,這個方法調用非常的不靠普。Fragment 在屏幕旋轉和返回堆棧(Backstack)中的時候,都會創建一個全新的 View,這個 onSaveInstanceState() 方法經常會出現不會被調用的情況,導致 Fragment 的狀態丟失。
我們來通過接下來的實例尋找解決方法。
首先,盡管已經有了一個類似 Activity 中的 onSaveInstanceState 方法,但是它顯然不能覆蓋所有情況。換種說法就是,你不能僅僅依賴於 onSaveInstanceState 方法來保存/恢復視圖的狀態。這裡有一些案例研究。
案例1:只有一個 Fragment 在棧中時,旋轉屏幕

屏幕旋轉是用來測試實例狀態的保存/恢復的最簡單的案例。這種情況很容易處理,你僅僅需要簡單地保存一些東西,比如:成員變量,它也會在屏幕旋轉時在 onSaveInstanceState 丟失,在 onActivityCreated 或者 onViewStateRestored 中恢復,如下所示:
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 不被回調,但是視圖重新生成。這意味著什麼?UI 裡的所有東西都沒了。下面就是這種案例。
案例2:後退棧(Back Stack)中的 Fragment

當一個 fragment 從後退棧中返回時(Fragment A就是在這種情況),Fragment A 中視圖將會遵循下圖的 Fragment 生命周期被重新創造出來。

你將會看到Fragment從後退棧中返回時,會回調 onDestroyview 方法和 onCreateview 方法。不管怎樣,顯然在這種情況 onSaveInstanceState 方法沒有被調用。結果就是 UI 裡的所有都沒有了,然後默認按照 Layout XML 文件中定義的來重新創建。
不管怎樣,實現了內在視圖狀態保存的視圖,如:帶有 android:freeezeText 的 EditText 或者 TextView,仍然能夠保存好視圖的狀態,這是因為 Fragment 實現了對內在視圖的狀態保存,但我們這些開發者不能抓住這些事件。我們唯一能做的就是在 onDestroyView 方法中手動保存實例狀態。
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
// Save State Here
}
@Override
public void onDestroyView() {
super.onDestroyView();
// Save State Here
}
問題也隨之而來,onDestroyView 不提供任何幫助來保存實例狀態到一個 Bundle,那我們應該把這些實例狀態保存到什麼地方呢? 答案就是 Argument, 它會隨著 Fragment 一直保存著。
現在代碼如下所示:
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 的狀態,在 restoreState 恢復狀態。現在已經看起來好多了不少。我們已經快結束了,但是還有一種怪異的情況。
案例3:在後退棧中超過一個 fragment 時,旋轉屏幕兩次

當你旋轉屏幕一次,onSaveInstanceState 會被回調,正如我們所期待的,UI 的狀態會被保存。但是當你旋轉屏幕超過一次,上述的代碼可能導致應用的崩潰。原因就是盡管當你旋轉屏幕時, onSaveInstanceState 方法被調用,但是在後退棧中的 fragment 會完全銷毀視圖,直到你浏覽返回到原來那個 fragment 才會重新創建。因此,你再次旋轉屏幕,就沒有視圖來保存狀態。當你試圖訪問那些不存在的視圖,saveState() 將會導致 NullPointerException,從而使應用崩潰。
方法就是檢查在 fragment 中視圖是否存在。如果存在那就保存,如果不存在,那就在 Argument 中 savedState 不需要保存,然後返回時保存。或者我們甚至不需要做任何事,因為在Argument 中已經做好了。
private void saveStateToArguments() {
if (getView() != null)
savedState = saveState();
if (savedState != null) {
Bundle b = getArguments();
b.putBundle(“savedState”, savedState);
}
}
哈,現在全都解決了!
Fragment 最終模版
如下就是我現在用於我工作中的 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) {
}
}
如果你使用這個模版,僅僅只需簡單地繼承這個類StatedFragment,然後在 onSaveState() 中保存事物,在 onRestoreState() 中恢復它們。上述代碼就會為你做好剩下的工作,我相信這已經覆蓋了我已知的可能情況。
setRetainInstance 能夠幫助開發者在布局改變時(如:屏幕旋轉)處理成員變量 而你可能注意到我沒有設置 setRetaionInstance 為 true。請記住,這就是我的目的,因為setRetainInstance(true)並沒有覆蓋全部的情況。最主要的原因是你不能一次又一次地保存那些經常背使用的嵌套 fragment 。所以我建議就是不要保存實例,除非你100%確定這個 fragment 不會用於嵌套。
用法:
好消息。這個博客講述的 StateFragment 現在加入了一個非常容易使用的庫,現在已經在 jcenter 上發布了。現在你可以簡單地在你工程的 build.gradle 文件中加上一個依賴。如下所示:
dependencies {
compile 'com.inthecheesefactory.thecheeselibrary:stated-fragment-support-v4:0.9.1'
}
繼承 StateFragment ,然後在 onSaveState(Bundle outState) 中保存狀態,在 onRestoreState(Bundle saveInstanceState)中恢復狀態。如果你想在 fragment 第一次啟動時做點什麼的話,你也可以覆蓋 onFirstTimeLaunched() 方法(在之後不會被調用)。
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
最後,不要忽略狀態保存的問題,在內存不足或者系統限制比較苛刻的機器上面,都有可能出現Fragment或Activity被回收,比如經常出現拍照之後返回App,但App裡面的數據被清空了,更常見的就是旋轉屏幕,所以要保存好自己需要的數據。
Android圓角圓形圖(支持各種scaleType 一)
看了看以前的項目,發現用了一個叫RoundImageView的類,當時直接從網上copy的(^__^) 嘻嘻……,今天偶爾看到了,覺得不能一直
android RadioButton和CheckBox組件的使用方法
RadioButton是單選按鈕,多個RadioButton放在一個RadioGroup控件中,也就是說每次只能有1個RadioButton被選中。而CheckBox是多
Android自定義View之Path解析
馬上要到五一了,提前祝賀大家五一快樂。今天的主角是路勁,這個詞給我的第一印象就是那種彎彎的小路,屆時讓我想起了小時候的曲徑通幽處,反正我是忘記了下一句是什麼。言歸正傳,A
Android MVVM
1、Android DataBinding:再見MVP,你好MVVM當我們談到android應用程序的架構模式時,MVP一直是占主流的地位。就像 Ted Mosby, N