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

嵌套Fragment的使用及常見錯誤

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

嵌套Fragments

嵌套Fragments Nested Fragments 是Android 4.2 API 17 引入的.
目的: 進一步加強動態複用.
若是要在Android 4.2以前使用, 能夠用support library v4的版本, 後面會有詳細的遷移過程介紹.java

嵌套Fragment的動態添加

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

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

一樣, 對於內部的fragment來講, getParentFragment() 方法能夠獲取到fragment的宿主fragment.git

getChildFragmentManager() 和 getFragmentManager()

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

也即, 若是fragment在activity中, fragment.getFragmentManager()獲得的是activity中管理fragments的那個manager.
若是fragment是嵌套在另外一個fragment中, fragment.getFragmentManager()獲得的是它的parent的getChildFragmentManager().app

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

使用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()之外, 其餘跟以前彷佛沒什麼區別.
若是嵌套的fragment不須要太多控制, 固定地佔據了一塊地方, 你可能想固然地爲了省事就把它放進了xml佈局文件裏, 寫個 標籤.
運行一下初看起來彷佛沒什麼錯, run一下也能顯示出來, 可是千萬不要這樣作, 多玩兩下更復雜的你就知道了.

上面官網介紹時就有這麼一句:

Note: You cannot inflate a layout into a fragment when that layout includes a <fragment>.
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中已經持有同一個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: 動態添加child fragment

解決上面的問題有各類方法, 最常規的作法是, 使用動態添加:

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工做以前調用它.
這兩個地方是發生異常的地方, 只要在其以前remove就好.

錯誤情形2: 把fragment放在一個動態佈局裏

把fragment放在一個動態佈局裏 -> java.lang.IllegalArgumentException: No view found for id

發現這個錯誤是由於項目中的一個子Fragment是添加在RecyclerView裏面的一塊的.
RecyclerView要等到Loader的數據取到了以後再populate每一塊的佈局.
仍是上面的流程, 啓動父fragment, load數據, 添加子fragment, 這都沒有問題.
可是一旦若是是上面的replace()addToBackStack() , 而且再次返回, 就會出現異常.

由於當重建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的根佈局裏, 而不是某個動態添加的佈局.

其餘

關於嵌套fragments的狀況, 可能和ViewPager結合使用的情形比較多.
這個感受說來話長了, 覺得有不少系統幫忙作的事情, 改天有空再說吧.

這裏有個大哥寫了個工具類Fragmentation.
他也有幾篇博文分析遇到的坑和緣由(見上面repo的README給出的連接), 裏面有一些back stack的問題, 還有動畫什麼的, 你們有興趣能夠看看.

參考資料

Guide: Nested Fragments

相關Demo

本文地址:
Android Fragment使用(二) 嵌套Fragments (Nested Fragments) 的使用及常見錯誤

相關文章
相關標籤/搜索