原文链接

Android 多个Fragment嵌套导致的三大BUG

Android Fragment使用(二) 嵌套Fragments (Nested Fragments) 的使用及常见错误

  • http://www.cnblogs.com/mengdd/p/5548359.html
  • http://www.cnblogs.com/mengdd/p/5552721.html
  • http://www.cnblogs.com/mengdd/p/5582244.html
  • http://www.cnblogs.com/mengdd/p/5590634.html

Android有碎片化的问题,当然本文说的碎片化不是指的系统版本碎片化的问题,而是Fragment组件碎片化的问题。

很久之前,在Android 3.1系统发布的时候,Google推出了使用Fragment来更加容易地开发平板和手机应用,虽然Activity还是页面结构的主体,但是却可以在其基础上使用多个Fragment来构建页面,这些Fragment都是有各自的生命周期的。

最常见的是列表和详情页面使用Fragment,如果在手机设备上,这个两个一般都是在独立的Activity页面中,但是在平板上这两个Fragment往往都是嵌套在一个Activity中。

当然,在开发过程中,正常情况下都是没有问题的。

1. 特殊的Fragment

如果想要使用Fragment来开发应用并且适配低版本系统,必须要使用Google提供的Support Library(V4).

Support Library这个兼容库设计地有些莫名其妙,它为了提供向后兼容的特性,替换了整个Fragment框架。比如,运行在3.1之后的系统系统上,使用的Fragment还是Support Library所提供的,而不是基于系统自身的(这一点在讲到后面的时候非常重要)。

2. Fragment的嵌套

使用Fragment时最繁琐复杂的就是多个Fragment之间的相互通信,必须通过Activity作为中间者传递。嵌套的Fragment一开始是不支持的,因为会导致了各式各样的bug。直到API 17,也就是Jelly Bean 4.2,终于开始支持嵌套的Fragments,并且这个功能也被添加到了Support Library里面。使用Fragment来搭建页面的梦想实现的一天终于到来了,这种方式有一个巨大的好处,就是解放Activity,使用多个Fragment组件来承载UI和逻辑。

梦想很美好,现实很残酷!

3. Fragment嵌套BUG之一:突变的动画效果

问题: 交互体验做到极致的APP,都会使UI具有平滑顺畅的动画效果。FragmentManager是允许通过设置转场过渡动画的。但是,退出动画会导致嵌套的Fragment在动画刚刚开始时就瞬间消失。

原因: Fragments有一个嵌套的生命周期,导致嵌套的Fragment会在其宿主Fragment前执行相应的生命周期,比如onStop。由于宿主Fragment的FragmentManager无法识别嵌套的Fragment,在动画开始执行的时候,嵌套的Fragment的视图树会直接跳过动画阶段,但是宿主Fragment的动画却还在执行。所以宿主Fragment和嵌套Fragment动画的步调是完全不一致的。

解决: 参考Stack Overflow上的一个解决方案:http://stackoverflow.com/questions/14900738/nested-fragments-disappear-during-transition-animation 原理是缓存宿主Fragment的当前可见状态,但是这个会导致页面重绘,可能衍生出其它的问题。

4. Fragment嵌套BUG之一:被继承的setRetainInstance

Fragments可以设置成保持状态。比如,当屏幕旋转导致Activity销毁和重启时,可以不用重新创建Fragment。

问题: 嵌套的Fragment会继承宿主Fragment的retain instance状态。

原因: 不明

解决: 尚无解决方案。

这个看起来是个很小的点,但是却可能产生很大的问题。虽然个人倾向于让所有fragments重新创建来保证其状态不出错(尤其是有复杂View的场景下),但是如果遇到不存在或简单View的场景是,比如网络请求或者多个组件调用,可能会设置一个回调监听器,而这个监听器是不需要重复创建的。上面所说的这种Fragment如果被嵌套在一个需要重新创建的Fragment里面,由于setRetainInstance 的继承性,会导致这个Fragment也跟着被重新创建。我的解决方式是使用静态实例和弱引用来持有这个Fragment,保证其不需要重新创建,有点坑。。。

5. Fragment嵌套BUG之一:错乱的onActivityResult传递

这是最让人头疼的问题了,而且我们会经常遇到,比如在嵌套的Fragment里面启动Activity。

问题: onActivityResult回调不会走到嵌套的Fragment里面。

原因: Support Library(V4)会修改了requestCode,使其中包含了一个Fragment 16位的索引值。这个索引值是与FragmentManager相关联的,Activity会根据这个索引值在自身的FragmentManager里面搜索Fragment来分发onActivityResult,但是只能搜寻到宿主Fragment,而宿主Fragment却不会向其内部嵌套的Fragment分发。这样就导致嵌套的Fragment永远收不到onActivityResult回调。

解决: 宿主Fragment向其内部嵌套的Fragment发送onActivityResult回调。

补充: 使用系统自带的Fragment不会出现这种问题,谷歌还是很牛逼的哈!

测试源码仓库:https://github.com/BurntBrunch/android-fragment-bugs

嵌套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 / 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.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的问题, 还有动画什么的, 大家有兴趣可以看看.

更多相关文章

  1. android 定义手势
  2. android LayoutParams 简单说明 理解 示例
  3. Android(安卓)动态生成布局的方式摘要
  4. Android透明状态栏(沉浸式状态栏)
  5. AndroidX之CoordinatorLayout+AppBarLayout顶部折叠栏
  6. Android毕设之Fragment+TabHost(三)
  7. android之GSON解析JSON
  8. 【面包屑】快速使用RecyclerView搭建列表
  9. 曾经的笔记——android的学习笔记(布局和电话、短信权限)

随机推荐

  1. Android仿QQ登录界面示例,实现登录、注册
  2. Android(安卓)5.0状态栏通知图标的实现
  3. (二)Unity 与 Android的布局管理
  4. Android(安卓)Studio 第八十三期 - Andro
  5. Android读取应用列表权限漏洞
  6. Android动态加载第三方APK的View研究过程
  7. Android(安卓)Studio 翻译插件Translatio
  8. Android中插件开发篇之----类加载器
  9. android 端全新指纹识别框架,适配 androi
  10. Android获取并显示图片