Android Fragment使用(二) 嵌套Fragments (Nested Fragments) 的使用及常見錯誤

嵌套Fragment的使用及常見錯誤

嵌套Fragments (Nested Fragments), 是在Fragment內部又添加Fragment.
使用時, 主要要依靠宿主Fragment的 getChildFragmentManager() 來獲取FragmentManger.
雖然看起來和在activity中添加fragment差不多, 但因為fragment生命周期及管理恢復模式不同, 其中有一些需要特別注意的地方.
本文內容還包括了從Fragment遷移到v4.Fragment代碼中需要改動的一些地方.

嵌套Fragments

嵌套Fragments Nested Fragments 是Android 4.2 API 17 引入的.
目的: 進一步增強動態(tài)復用.
如果要在Android 4.2之前使用, 可以用support library v4的版本, 后面會有詳細的遷移過程介紹.

嵌套Fragment的動態(tài)添加

在宿主fragment里調用getChildFragmentManager()
即可用它來向這個fragment內部添加fragments.

Fragment videoFragment = new VideoPlayerFragment();
FragmentTransaction transaction = getChildFragmentManager().beginTransaction();
transaction.add(R.id.video_fragment, videoFragment).commit();

同樣, 對于內部的fragment來說, getParentFragment() 方法可以獲取到fragment的宿主fragment.

getChildFragmentManager() 和 getFragmentManager()

getChildFragmentManager()是fragment中的方法, 返回的是管理當前fragment內部子fragments的manager.
getFragmentManager()在activity和fragment中都有.
在activity中, 如果用的是v4 support庫, 方法應該用getSupportFragmentManager(), 返回的是管理activity中fragments的manager.
在fragment中, 還叫getFragmentManager(), 返回的是把自己加進來的那個manager.

也即, 如果fragment在activity中, fragment.getFragmentManager()得到的是activity中管理fragments的那個manager.
如果fragment是嵌套在另一個fragment中, fragment.getFragmentManager()得到的是它的parent的getChildFragmentManager().

總結就是: getFragmentManager()是本級別管理者, getChildFragmentManager()是下一級別管理者.
這實際上是一個樹形管理結構.

使用Support library

為什么要使用support library? 有兩種原因:

  1. 要在API level11之前使用fragment.
  2. 要在API Level 17之前使用getChildFragmentManager(), 即使用嵌套Fragment.

遷移到support library需要改動哪些地方?

把Fragment遷移到v4版本, 需要改動如下地方:

import android.app.Fragment; -> import android.support.v4.app.Fragment;
Activity -> FragmentActivity / AppCompatActivity
activity.getFragmentManager() -> getSupportFragmentManager()

Loader, LoaderManager, LoaderCursor也需要改成v4包的.
activity.getLoaderManager() -> getSupportLoaderManager()

Fragment中onTrimMemory()方法不見了
以前是這個方法

    @Override
    public void onTrimMemory(int level) {
        super.onTrimMemory(level);
        imageLoader.trimMemory(level);
    }

v4版本需要改成這個

   @Override
    public void onLowMemory() {
        super.onLowMemory();
        imageLoader.trimMemory(ComponentCallbacks2.TRIM_MEMORY_COMPLETE);
    }

嵌套Fragment使用常見錯誤

錯誤情形1: 把嵌套Fragment放在布局里

把嵌套Fragment放在布局里 -> InflateException in Binary XML

看起來嵌套fragment的使用除了要用getChildFragmentManager()以外, 其他跟之前似乎沒什么區(qū)別.
如果嵌套的fragment不需要太多控制, 固定地占據(jù)了一塊地方, 你可能想當然地為了省事就把它放進了xml布局文件里, 寫個標簽.
運行一下初看起來似乎沒什么錯, run一下也能顯示出來, 但是千萬不要這樣做, 多玩兩下更復雜的你就知道了.

上面官網(wǎng)介紹時就有這么一句:

Note: You cannot inflate a layout into a fragment when that layout includes a .
Nested fragments are only supported when added to a fragment dynamically.

人家這么說肯定是有原因的哇, 下面我來告訴你我知道的問題:
如果Fragment被嵌套寫在了布局里, inflate到這個標簽的時候就相當于將它加進了FragmentManager里.
如果嵌套的parent fragment因為需要重建View而重新走了onCreateView()方法, 再次inflate, 此時就會拋出異常: InflateException in Binary XML

之前為什么可以呢? 非嵌套的情況, fragment直接加在activity里, 如果需要重新inflate, 必定是在onCreate()里, activity是重新建的, 所以沒有問題, 因為不存在fragmentManager中已經(jīng)持有同一個fragment的問題.

舉一個例子:
在嵌套的情況下, 如果FragmentE布局里有FragmentA, 這時候我們需要疊加一個FragmentD.
用了replace(), 并且addToBackStack().
當D顯示的時候, E實際上View是被銷毀的, 然后back回來, 重建View, 即FragementE需要重新從onCreateView
()開始走生命周期, 走到inflate的時候又看到了fragmentA的標簽.
但是這時候A實際上還在FragmentManager里面, 所以就會拋出如下的異常:
android.view.InflateException: Binary XML file line # XX: Binary XML file line #XX: Error inflating class fragment
崩潰的位置就在parent fragment(FragmentE) inflate的時候.
打印具體的異常棧信息可以看到:

at com.example.ddmeng.helloactivityandfragment.fragment.FragmentE.onCreateView(FragmentE.java:35)
at android.app.Fragment.performCreateView(Fragment.java:2220)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:973)
at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1148)
at android.app.FragmentManagerImpl.popBackStackState(FragmentManager.java:1587)
at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:578)
at android.support.v4.app.BaseFragmentActivityEclair.onBackPressedNotHandled(BaseFragmentActivityEclair.java:27)
at android.support.v4.app.FragmentActivity.onBackPressed(FragmentActivity.java:189)
 Caused by: java.lang.IllegalArgumentException: Binary XML file line #16: Duplicate id 0x7f0c0059, tag null, or parent id 0xffffffff with another fragment for com.example.ddmeng.helloactivityandfragment.fragment.FragmentA
at android.app.FragmentManagerImpl.onCreateView(FragmentManager.java:2205)

實驗例子代碼

Solution 1: 動態(tài)添加child fragment

解決上面的問題有各種方法, 最常規(guī)的做法是, 使用動態(tài)添加:

Fragment fragmentA = getChildFragmentManager().findFragmentByTag(NESTED_FRAGMENT_TAG);
if (fragmentA == null) {
    Log.i(LOG_TAG, "add new FragmentA !!");
    fragmentA = new FragmentA();
    FragmentTransaction fragmentTransaction = getChildFragmentManager().beginTransaction();
    fragmentTransaction.add(R.id.fragment_container, fragmentA, NESTED_FRAGMENT_TAG).commit();
} else {
    Log.i(LOG_TAG, "found existing FragmentA, no need to add it again !!");
}

Solution 2: 在異常之前remove child fragment

如果你的子fragment非要加在布局里不可, 而你的程序確實會有重建父fragment view的情形.
為了避免上面的異常, 你也可以這樣做(tricky and not recommended):

public void removeChildFragment(Fragment parentFragment) {
    FragmentManager fragmentManager = parentFragment.getChildFragmentManager();
    Fragment child = fragmentManager.findFragmentById(R.id.child);
    if (child != null) {
        fragmentManager.beginTransaction()
        .remove(child)
        .commitAllowingStateLoss();
    }
}

在parentFragment的onCreateView()方法中inflate之前和onSaveInstanceState()方法中做save工作之前調用它.
這兩個地方是發(fā)生異常的地方, 只要在其之前remove就好.

錯誤情形2: 把fragment放在一個動態(tài)布局里

把fragment放在一個動態(tài)布局里 -> java.lang.IllegalArgumentException: No view found for id

發(fā)現(xiàn)這個錯誤是因為項目中的一個子Fragment是添加在RecyclerView里面的一塊的.
RecyclerView要等到Loader的數(shù)據(jù)取到了之后再populate每一塊的布局.
還是上面的流程, 啟動父fragment, load數(shù)據(jù), 添加子fragment, 這都沒有問題.
但是一旦如果是上面的replace()addToBackStack() , 并且再次返回, 就會出現(xiàn)異常.

因為當重建View的時候, fragmentManager其中是持有child fragment的, 但是找不到它的container, 于是就會拋出異常.
我也同樣做了一個小實驗, 在我的demo程序里:
HelloActivityAndFragment
Nested Fragment in Dynamic Container:
在Fragment F中, 先添加一個FrameLayout, 再把child fragment A加進去.
然后在Activity中, 用D replace F, 按back鍵返回, 就會有crash:

     java.lang.IllegalArgumentException: No view found for id 0x7f0c0062 (com.example.ddmeng.helloactivityandfragment:id/frame_container) for fragment FragmentA{b37763 #0 id=0x7f0c0062 FragmentA}
         at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:965)
         at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1148)
         at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1130)
         at android.app.FragmentManagerImpl.dispatchActivityCreated(FragmentManager.java:1953)
         at android.app.Fragment.performActivityCreated(Fragment.java:2234)
         at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:992)
         at android.app.FragmentManagerImpl.moveToState(FragmentManager.java:1148)
         at android.app.BackStackRecord.popFromBackStack(BackStackRecord.java:1670)
         at android.app.FragmentManagerImpl.popBackStackState(FragmentManager.java:1587)
         at android.app.FragmentManagerImpl.popBackStackImmediate(FragmentManager.java:578)
         at android.app.Activity.onBackPressed(Activity.java:2503)

這是因為返回的時候FragmentManager找不到對應的container了.
所以應該避免這種做法, 盡量把fragment加進parent的根布局里, 而不是某個動態(tài)添加的布局.

其他

關于嵌套fragments的情況, 可能和ViewPager結合使用的情形比較多.
這個感覺說來話長了, 以為有很多系統(tǒng)幫忙做的事情, 改天有空再說吧.

這里有個大哥寫了個工具類Fragmentation.
他也有幾篇博文分析遇到的坑和原因(見上面repo的README給出的鏈接), 里面有一些back stack的問題, 還有動畫什么的, 大家有興趣可以看看.

參考資料

Guide: Nested Fragments

相關Demo

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末臀叙,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 212,185評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件驱入,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機缆娃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,445評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來瑰排,“玉大人贯要,你說我怎么就攤上這事⊥肿。” “怎么了崇渗?”我有些...
    開封第一講書人閱讀 157,684評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長京郑。 經(jīng)常有香客問我宅广,道長,這世上最難降的妖魔是什么些举? 我笑而不...
    開封第一講書人閱讀 56,564評論 1 284
  • 正文 為了忘掉前任乘碑,我火速辦了婚禮,結果婚禮上金拒,老公的妹妹穿的比我還像新娘兽肤。我一直安慰自己,他們只是感情好绪抛,可當我...
    茶點故事閱讀 65,681評論 6 386
  • 文/花漫 我一把揭開白布资铡。 她就那樣靜靜地躺著,像睡著了一般幢码。 火紅的嫁衣襯著肌膚如雪笤休。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,874評論 1 290
  • 那天症副,我揣著相機與錄音店雅,去河邊找鬼。 笑死贞铣,一個胖子當著我的面吹牛闹啦,可吹牛的內容都是我干的。 我是一名探鬼主播辕坝,決...
    沈念sama閱讀 39,025評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼窍奋,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起琳袄,我...
    開封第一講書人閱讀 37,761評論 0 268
  • 序言:老撾萬榮一對情侶失蹤江场,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后窖逗,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體址否,經(jīng)...
    沈念sama閱讀 44,217評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 36,545評論 2 327
  • 正文 我和宋清朗相戀三年碎紊,在試婚紗的時候發(fā)現(xiàn)自己被綠了佑附。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,694評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡矮慕,死狀恐怖,靈堂內的尸體忽然破棺而出啄骇,到底是詐尸還是另有隱情痴鳄,我是刑警寧澤,帶...
    沈念sama閱讀 34,351評論 4 332
  • 正文 年R本政府宣布缸夹,位于F島的核電站痪寻,受9級特大地震影響,放射性物質發(fā)生泄漏虽惭。R本人自食惡果不足惜橡类,卻給世界環(huán)境...
    茶點故事閱讀 39,988評論 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望芽唇。 院中可真熱鬧顾画,春花似錦、人聲如沸匆笤。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,778評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炮捧。三九已至庶诡,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間咆课,已是汗流浹背末誓。 一陣腳步聲響...
    開封第一講書人閱讀 32,007評論 1 266
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留书蚪,地道東北人喇澡。 一個月前我還...
    沈念sama閱讀 46,427評論 2 360
  • 正文 我出身青樓,卻偏偏與公主長得像殊校,于是被迫代替她去往敵國和親撩幽。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,580評論 2 349

推薦閱讀更多精彩內容