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 引入的.
目的: 进一步增强动态复用.
如果要在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? 有两种原因:

  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 .
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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,711评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 83,932评论 2 376
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,770评论 0 330
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,799评论 1 271
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,697评论 5 359
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,069评论 1 276
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,535评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,200评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,353评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,290评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,331评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,020评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,610评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,694评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,927评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,330评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,904评论 2 341

推荐阅读更多精彩内容