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


 

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

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

嵌套Fragment時Duplicated id或者Tag之癢

這是一個小坑,但是初學者很容易遇到,特別是在Fragment之中套有Fragment時,且又是布局中添加子Fragment時更容易遇到。

現象:

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中查找對象是否存在,不存在時再創建,也即:

      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。

可見性之疼

現象:

當有多個Fragment層疊在一起時,每個Fragment如何能感知其對用戶的可見性。比如應用有三個頁面,A,B和C,比如A是整體類別列表,B是每個類別的詳情,C又是類別的某種更詳細的信息,當C顯示出來時,A和B怎么能知道它其實對於用戶已經不可見了,所以就可以不刷新,不加載數據等等。當C被用戶BACK后,B又如何感覺它變成可見了?

原因:

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); } 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM