Android實戰技巧:Fragment的那些坑(轉)

 

原文地址:http://toughcoder.net/blog/2015/04/30/android-fragment-the-bad-parts/?utm_source=tuicool&utm_medium=referral
html

Fragment是Android在3.0(Homeycomb)版本時加入的用以更靈活的構建多屏幕界面的可UI組件。關於Fragment以基本使用方法能夠參考官方的教程最佳實踐,以及選擇Activity仍是Fragment。 可是Fragment使用起來卻遠沒有教程中說的那樣簡單,也遠比Activity要複雜一些,這裏總結了孤在使用Fragment時所遇到的坑。java

嵌套Fragment時Duplicated id或者Tag之癢

這是一個小坑,可是初學者很容易遇到,特別是在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重複。這麼作的目的就是減小對象的建立,盡能夠的複用對象。異步

如何破解:

  1. 在佈局中寫fragment時,不要添加id或者tag;
  2. 若是非要添加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();

replace之痛

現象:

當有二個相同的總體頁面層疊時,想把最後一個佈局中的某個用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增長了,仍是減小了,並不能知道。

如何破解:

這個一個很是使人蛋疼的問題,簡單的頁面還好,可是涉及到數據加載或者要針對某些事件(網絡)刷新時就有問題了,對用戶不可見的頁面不必刷新。可行的解法就是:

  1. 監聽FragmentManager的BackStackState的改變
  2. 定義頁面路徑深度而後與BackStack深度比較,以感知是否對用戶可見 如前面A是一級,其path爲1,B是2,C是3。當前Stack深度爲3時,C是可見的,A與B不可見,以此類推。

空白區域的點擊之膿

現象:

一個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,這會讓根佈局把點擊事件吃掉,以防止事件會繼續傳遞下去,形成上面的狀況。

Activity從新建立之殤

現象:

這個沒有通常性的錯誤,只會有與項目相關的具體的錯誤異常,或者頁面顯示不正確。以及爲何教程中都有這麼一句:

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重建時恢復狀態,須要:

  1. 對於Activity

    要在onSaveInstanceState()時,把一些變量保存,而後在onCreate時恢復

  2. 對於Fragment

    告訴系統,你想恢復狀態Fragment#setRetainInstance(true)。而後,也在onSavedInstance()中保存狀態,在onCreate時恢復。 這就夠了,系統會在從新建立Activity時把其所持有的Fragment也建立出來。因此爲何每一個Fragment子類都須要定義一個默認的Constructor。更多的能夠參考這篇文章

FragmentTransaction的異步操做之殤

FragmentTransaction是異步的,commit()僅是至關於把操做加入到FragmentManager的隊列,而後FragmentManager會在某一個時刻來執行,並非當即執行。因此,真正開始執行commit()時,若是Activity的生命週期發生了變化,好比走到了onPause,或者走到了onStop,或者onDestroy都走完了,那麼就會報出IllegalStateException。

還有一個異步的緣由就是,在異步中操做(顯示)Fragment。好比,先去網絡請求數據,而後根據數據顯示一個Fragment,這個特別容易出現的狀況是網絡請求回來了,可是Activity已經不在了,這時若是commit也會報出IllegalStateException。

具體的緣由,以及如何避免能夠參考大牛的這篇文章

常見的解法就是做者建議的:1. 當心在生命週期中commit 。2 儘可能不要在異步回調中commit 另外的解法 就是

  • 在異步回調中判斷Activity是否在銷燬中,isFinishing,若是true,就中止作其餘事情
  • 儘量把異步任務控制在活動的生命週期內(onStart->onStop)。當出現stop時終止異步任務。再次start時再次啓動。

    可是這個並不適用全部狀況。好比按HOME的狀況,一般這個過程不須要把任務停掉。由於通常狀況下,再切回來時,應用應該保持切走時的狀態,好比,加載一個數據,按HOME切走,再回來時,應該加載完成。這也正是多任務系統的一個表現。 若是onstop時停掉任務,那麼要作不少工做來在onstart時恢復狀態。

  • 使用commitAllowStateLoss() 這個是最終方案。除了從設計 上避免之外,這是惟 一的方式。

噁心的Activity重建以及恢復其Fragment

首先說安卓系統很是噁心的一點就是某些狀況下系統會殺掉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); } 
相關文章
相關標籤/搜索