標籤:net dynamic mac java blog 內容 pop hub view
嵌套Fragment的使用及常見錯誤
嵌套Fragments (Nested Fragments), 是在Fragment內部又添加Fragment.
使用時, 主要要依靠宿主Fragment的 getChildFragmentManager() 來擷取FragmentManger.
雖然看起來和在activity中添加fragment差不多, 但因為fragment生命週期及管理復原模式不同, 其中有一些需要特別注意的地方.
本文內容還包括了從Fragment遷移到v4.Fragment代碼中需要改動的一些地方.
嵌套Fragments
嵌套Fragments Nested Fragments 是Android 4.2 API 17 引入的.
目的: 進一步增強動態複用.
如果要在Android 4.2之前使用, 可以用support library v4的版本, 後面會有詳細的遷移過程介紹.
嵌套Fragment的動態添加
在宿主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? 有兩種原因:
- 要在API level11之前使用fragment.
- 要在API Level 17之前使用
getChildFragmentManager(), 即使用嵌套Fragment.
遷移到support library需要改動哪些地方?
把Fragment遷移到v4版本, 需要改動如下地方:
import android.app.Fragment; -> import android.support.v4.app.Fragment;Activity -> FragmentActivity / AppCompatActivityactivity.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.FragmentAat 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) 的使用及常見錯誤
Android Fragment使用(二) 嵌套Fragments (Nested Fragments) 的使用及常見錯誤