Fragment是Android在3.0(Homeycomb)版本時加入的用以更靈活的構建多屏幕界面的可UI組件。關於Fragment以基本使用方法能夠參考官方的教程和最佳實踐,以及選擇Activity仍是Fragment。 可是Fragment使用起來卻遠沒有教程中說的那樣簡單,也遠比Activity要複雜一些,這裏總結了孤在使用Fragment時所遇到的坑。java
這是一個小坑,可是初學者很容易遇到,特別是在Fragment之中套有Fragment時,且又是佈局中添加子Fragment時更容易遇到。android
Fragment中套有另外一個Fragment,當第二次進入父Fragment時或者由Fragment建立的界面時會拋異常,大體意思是子Fragment的Id或Tag重複了。若是你在layout中給子fragment加了id或者tag,那麼必定會遇到此異常。網絡
在添加Fragment時均可覺得Fragment指定一個Id或者Tag用以標識這個Fragment。由於每一個Activity所附帶的Fragment都是放在一個對象池中,在Activity的生命週期裏,Fragment仍然在池中,即便是把某一個Fragment從Activity中detach掉(也即用FragmentManager pop掉),這個池是由FragmentManager來管理的。當你再次要以某個id或者Tag添加Fragment時,FragmentManager會在池中檢索,若是發現已經存在Fragment對象帶有此Id或者Tag時,就會拋此異常並報怨Id重複。這麼作的目的就是減小對象的建立,盡能夠的複用對象。異步
若是非要添加id或者tag,就在代碼中添加fragment,如使用Id或者Tag時,先到FragmentManager中查找對象是否存在,不存在時再建立,也即:ide
Fragment target = getFragmentManager().findFragmentByTag("tag"); if (target == null) { targe = new SomeFragment(); } FragmentTransaction ft = getFragmentManager().beginTransaction(); ft.add(R.id.content, target, "tag"); ft.commit();
當有二個相同的總體頁面層疊時,想把最後一個佈局中的某個用Fragment來replace,會發現,它把前面的replace,後面的沒效果。函數
佈局的Id在一個窗體(Activity)中是惟一的,Fragment的replace也是使用此惟一的Id來把相應佈局替換成Fragment的。當相同的頁面層疊時,同一個Id的佈局出現了二次,但Id是同樣的。因此FragmentTransaction在replace時僅替換了一個。而不會像期待的那樣,替換最後一個頁面。佈局
若是相同的頁面非要層疊,要麼不使用Fragment,要麼爲佈局設置不一樣的Id。這種狀況多出如今佈局的複用上面,好比某二個頁面長的像,因此複用了同一總體佈局。但實際的邏輯上不是相同的頁面,徹底能夠爲佈局設置不一樣的Id。ui
當有多個Fragment層疊在一塊兒時,每一個Fragment如何能感知其對用戶的可見性。好比應用有三個頁面,A,B和C,好比A是總體類別列表,B是每一個類別的詳情,C又是類別的某種更詳細的信息,當C顯示出來時,A和B怎麼能知道它其實對於用戶已經不可見了,因此就能夠不刷新,不加載數據等等。當C被用戶BACK後,B又如何感受它變成可見了?spa
Fragment的生命週期與Activity是同樣的,添加到Activity會把OnCreate相似的回調走一遍,而後,Activity onResume/onPause/onstart/onStop時,其所持有的Fragment也走相應的onResume/onPause/onstart/onPause。可是Fragment與Activity很是不一樣的是,Activity當有另外一個Activity顯示時,當前的Activity會走onPause/onStop,而Fragment則徹底沒有感知。最多隻能從FragmentManager那裏知道BackStackState改變了,可是是Fragment增長了,仍是減小了,並不能知道。
這個一個很是使人蛋疼的問題,簡單的頁面還好,可是涉及到數據加載或者要針對某些事件(網絡)刷新時就有問題了,對用戶不可見的頁面不必刷新。可行的解法就是:
一個Fragment,層疊在另一個Fragment或者Activity之上,此Fragment中有一些空白區域,也即Widget以外的空白區域,當點擊這些空白區域的時候發現這個Fragment下面的Fragment或者Activity中的View收到了事件而且響應了點擊事件。
Fragment的本質就是一個View佈局的管理器,當Fragment attach到Activity時,其實就是把Fragment#onCreateView()返回的View,替換掉(若是是用replace)FragmentTransaction#replace中指定的View,或者添加到(若是是add)FragmentTransaction#add()中指定的ViewGroup裏面。
當咱們以層疊方式顯示多個Fragment時,一般的作法就是弄一個FrameLayout,而後每次把Fragment add到此佈局。所以,這時Activity的頁面佈局樹實際上就是一個FrameLayout裏面包含幾個View。
因此,當點擊上面Fragment的空白區域時,若是事件沒被吃掉,就會向下傳遞。
在Fragment的根佈局加上一個clickable=true,這會讓根佈局把點擊事件吃掉,以防止事件會繼續傳遞下去,形成上面的狀況。
這個沒有通常性的錯誤,只會有與項目相關的具體的錯誤異常,或者頁面顯示不正確。以及爲何教程中都有這麼一句:
1
2 3 4 5 6 |
@Override onCreate(Bundle savedInstance) { if (savedIntance == null) { // create fragment and add it to Activity. } } |
Activity除了正常啓動走到onCreate,還有另外的入口,好比系統配置信息發生變化時,或者Activity在棧比較深的地方,系統會把Activity殺掉,而後再從新建立它,問題就是在這個從新建立。從新建立與新建一個Activity不一樣,它是要儘量的恢復先前所在的狀態,由於這對用戶來講是透明的,也就是說不能讓用戶感知到,不然體驗會至關差。惟一與常規建立的區別就在於傳給onCreate的參數savedInstanceState是否是null.
爲了能在Activity重建時恢復狀態,須要:
對於Activity
要在onSaveInstanceState()時,把一些變量保存,而後在onCreate時恢復
對於Fragment
告訴系統,你想恢復狀態Fragment#setRetainInstance(true)。而後,也在onSavedInstance()中保存狀態,在onCreate時恢復。 這就夠了,系統會在從新建立Activity時把其所持有的Fragment也建立出來。因此爲何每一個Fragment子類都須要定義一個默認的Constructor。更多的能夠參考這篇文章。
FragmentTransaction是異步的,commit()僅是至關於把操做加入到FragmentManager的隊列,而後FragmentManager會在某一個時刻來執行,並非當即執行。因此,真正開始執行commit()時,若是Activity的生命週期發生了變化,好比走到了onPause,或者走到了onStop,或者onDestroy都走完了,那麼就會報出IllegalStateException。
還有一個異步的緣由就是,在異步中操做(顯示)Fragment。好比,先去網絡請求數據,而後根據數據顯示一個Fragment,這個特別容易出現的狀況是網絡請求回來了,可是Activity已經不在了,這時若是commit也會報出IllegalStateException。
具體的緣由,以及如何避免能夠參考大牛的這篇文章。
常見的解法就是做者建議的:1. 當心在生命週期中commit 。2 儘可能不要在異步回調中commit 另外的解法 就是
儘量把異步任務控制在活動的生命週期內(onStart->onStop)。當出現stop時終止異步任務。再次start時再次啓動。
可是這個並不適用全部狀況。好比按HOME的狀況,一般這個過程不須要把任務停掉。由於通常狀況下,再切回來時,應用應該保持切走時的狀態,好比,加載一個數據,按HOME切走,再回來時,應該加載完成。這也正是多任務系統的一個表現。 若是onstop時停掉任務,那麼要作不少工做來在onstart時恢復狀態。
首先說安卓系統很是噁心的一點就是某些狀況下系統會殺掉Activity,而後從新建立並嘗試恢復其先前的狀態,好比當旋轉屏幕時,當系統語言發生變化時,當棧中的Activity被回收了,又到棧頂時等等,這點很是噁心,經常帶來問題。識別重建與新建的方法就是看onCreate中的Bundle參數是否是null。
對於FragmentActivity,更加噁心,此種場景時,它在onSaveInstance時會保存Fragment,而後在onCreate時會從新建立,會調用Framgment的默認無參構造來建立Fragment對象。因此這也是爲何文檔中說Fragment必定要有一個默認的構造函數,並且最好不要有帶參數的構造函數,傳參數要用setArguments。默認構造函數的緣由是爲了重建Fragment實例。setArguments的參數是一個Bundle也會跟隨Fragment保存起來,在重建Fragment時會幫你恢復。這裏的恢復狀態的數據的保存都是經過Binder方式保存在系統中,這也說明爲啥參數非要是一個Bundle。
那麼問題來了,當你確實須要帶參數的構造函數,或者說系統沒法幫你重建Fragment(好比Fragment要從動態加載的Dex中獲取)時怎麼辦呢?
首先,咱們要模擬這一場景,最方便的就是把activity的configChanges去掉,而後旋轉屏幕。
一個思路就是阻止系統恢復Fragment,咱們能夠本身來加載,由於重建也會走到Activity的onCreate,因此咱們有理由重走一遍初始化流程。怎麼阻止呢,就是在FragmentActivity保存全部Fragment狀態前把Fragment從FragmentManager中移除掉。
1
2 3 4 5 6 7 |
@Override public void onSaveInstance(Bundle out) { FragmentTransaction ft = getSupportFragmentManager().benginTransaction(); ft.remove(frag); ft.commitAllowStateLoss(); super.onSaveInstance(out); } |