Android中儲存和恢複Fragment狀態的最好方法,androidfragment
英文原文:Probably be the best way (?) to save/restore Android Fragment’s state so far
關鍵點:Fragment的Arguments。
經過這幾年使用Fragment之後,我想說,Fragment的確是一種充滿智慧的設計,但是使用Fragment時有太多需要我們逐一解決的問題,尤其是在處理資料保持的時候。
首先,雖然其有類似於activity的onSaveInstanceState,但是別想僅僅靠onSaveInstanceState就能保持資料。
下面就是一些案例:
情景一:stack中只有一個Fragment的時候旋轉螢幕
是的,旋轉螢幕是測試資料保持最簡單的方法。這種情況非常簡單,你只需在onSaveInstanceState儲存會在旋轉的時候會丟失的資料,包括變數,然後在onActivityCreated或者onViewStateRestored中取出來:
int someVar;@Overrideprotected void onSaveInstanceState(Bundle outState) { outState.putInt("someVar", someVar); outState.putString(“text”, tv1.getText().toString());}@Overridepublic void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); someVar = savedInstanceState.getInt("someVar", 0); tv1.setText(savedInstanceState.getString(“text”));}
看起來很簡單是吧,但是存在這樣的情況,View重建,但是onSaveInstanceState未被調用,這意味著UI上的所有東西都丟失了,請看下面的案例。
情景2:Fragment從回退棧的返回
當fragment從backstack中返回(這裡是Fragment A),根據 官方文檔 對Fragment生命週期的描述,Fragment A中的view會重建。
從這張圖可以看到,當Fragment從回退棧中返回的時候,onDestroyView 和 onCreateView被調用,但是onSaveInstanceState貌似沒有被調用,這就導致了一切UI資料都回到了xml布局中定義的初始狀態。當然,那些內部實現了狀態儲存的view,比如有android:freezeText屬性的EditText和TextView,仍然可以保持其狀態,因為Fragment可以為他們保持資料,但是開發人員沒法獲得這些事件,我們只能手動的在onDestroyView中儲存這些資料。
大概流程如下:
@Overridepublic void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // 這裡儲存資料}@Overridepublic void onDestroyView() { super.onDestroyView(); // 如果onSaveInstanceState沒被調用,這裡也可以儲存資料}
但是問題來了onSaveInstanceState中儲存資料很簡單,因為它有Bundle參數,但是onDestroyView沒有,那儲存在哪裡呢?答案是能和Fragment一起共存的Argument。
代碼大致如下:
Bundle savedState;@Overridepublic void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); // Restore State Here if (!restoreStateFromArguments()) { // First Time running, Initialize something here }}@Overridepublic void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); // Save State Here saveStateToArguments();}@Overridepublic 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;}/////////////////////////////////// 取出狀態資料/////////////////////////////////private void restoreState() { if (savedState != null) { //比如 //tv1.setText(savedState.getString(“text”)); }}//////////////////////////////// 儲存狀態資料//////////////////////////////private Bundle saveState() { Bundle state = new Bundle(); // 比如 //state.putString(“text”, tv1.getText().toString()); return state;}
現在你可以輕鬆的在fragment的saveState和restoreState中分別儲存和取出資料了。現在看起來好多了,幾乎快要成功了,但是還有更極端的情況。
情景3:在回退棧中有一個以上的Fragment的時候旋轉兩次
當你旋轉一次螢幕,onSaveInstanceState被調用,UI的狀態會如預期的那樣被儲存,,但是當你再一次旋轉螢幕,上面的代碼就可能會崩潰。原因是雖然onSaveInstanceState被調用了,但是當你旋轉螢幕,回退棧中Fragment的view將會銷毀,同時在返回之前不會重建。這就導致了當你再一次旋轉螢幕,沒有可以儲存資料的view。saveState()將會引用到一個不存在的view而導致null 指標異常NullPointerException,因此需要先檢查view是否存在。如果存在儲存其狀態資料,將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()中取出資料,其餘的事情上面的代碼已經為你做好了,我相信覆蓋了我所知道的所有情況。
庫
現在本文描述的StatedFragment已經被做成了一個便於使用的庫,並且發布到了jcenter,你現在只需在build.gradle中添加依賴就行了:
dependencies { compile 'com.inthecheesefactory.thecheeselibrary:stated-fragment-support-v4:0.9.1'}
繼承StatedFragment,同時分別在onSaveState(Bundle outState)和onRestoreState(Bundle savedInstanceState)中儲存和取出狀態資料。如果你想在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")); } ... }
首發地址 http://jcodecraeer.com/a/anzhuokaifa/androidkaifa/2015/0327/2648.html