引言
本文首先回顾一下安卓Fragment的基本概念、使用、通信和生命周期等,然后结合开发过程中遇到的坑,记录一下解决办法。
Fragment基本概念
Fragment的添加
方法一: 布局里的标签
标识符: tag, id, 如果都没有, container的id将会被使用.
方法二: 动态添加
动态添加利用了一个transaction:
commit()方法并不立即执行transaction中包含的动作,而是把它加入到UI线程队列中.
如果想要立即执行,可以在commit之后立即调用FragmentManager的executePendingTransactions()方法.
commit()方法必须在状态存储之前调用,否则会抛出异常,如果觉得状态丢失没关系,可以调用commitAllowingStateLoss(). 但是除非万不得已, 一般不推荐用这个方法, 会掩盖很多错误.
Back Stack
Activity的back stack: 系统维护, 每个task一个back stack.
Fragment的back stack: 宿主activity掌管, 每个activity一个.
通过调用addToBackStack(),commit()的一系列转换作为一个transaction被存储在back stack中,
用户按Back键, 从栈中pop出一个transaction, 逆转操作, 可以返回上一个转换前的状态.
一个transaction可以包含多种操作, 并且不局限于对同一个Fragment, 所以每一个transaction实际上可以是一系列对多个fragment的操作的组合.
加入到back stack中去的时候, 是把这一系列的组合作为一个原子, 加入到back stack中.
构造和参数传递
所有的Fragment都必须有一个public的无参构造函数, 因为framework经常会在需要的时候重新创建实例(状态恢复时), 它需要的就是这个构造.
如果无参构造没有提供,会有异常.
所以不要给Fragment写有参数的构造函数, 也不要企图搞个什么单例的Fragment. 这些都是反设计的.
参数传递的正确姿势:
|
|
这里是提供了一个静态方法, 也可以new出对象后自己set Bundle参数.
Fragment的通信
除了DialogFragment和嵌套Fragment需要与自己的parent fragment通信以外, 一般的fragment是不与其他fragment有任何通信的. 因为要求应尽量独立, 模块化, 可复用.
fragment与自己的parent activity (除了嵌套和dialog的情况外, 这个parent通常是activity) 有直接通信, 一般以这三种方式:
在构造fragment的时候, 通过Bundle传递参数.
parent可以直接调用fragment的public方法, 这里也可以传递一些参数.
Listener, 也即parent实现的callback接口, fragment可以在自己内部调用, 这里fragment也可以传递参数出去.
Fragment的生命周期
Fragment的生命周期首先和Activity的生命周期密切相关,
如果activity stopped,其中所有的fragment都不能start;
如果activity destroyed, 其中所有的fragment都会被destroyed.
只有activity在resumed状态下,fragment的生命周期可以独立改变,否则它被activity控制.




FragmentTransaction基础操作
FragmentTransaction 中对Fragment有如下几种操作:
除了replace()以外其他都是成对的.
其中attach()和detach()不是很常用.
调用detach()之后, fragment实际的生命周期会走到onDestroyView(), 但不会走onDestroy()和onDetach(), 也即fragment本身并没有被销毁, 只是view被销毁了. 这和addToBackStack()的情况一样, 尽管调用detach()的时候没有addToBackStack(), 仍然只是走到view被销毁的阶段.
add()和remove()是将fragment添加和移除.
remove()比detach()要彻底一些, 如果不加入到back stack, remove()的时候, fragment的生命周期会一直走到onDetach().
show()和hide()是用来设置fragment的显示和隐藏状态, 这两个方法并不对应fragment的状态变化,只是将view设置为visible和gone,然后调用onHiddenChanged()的回调.
实际上replace() == remove() + add(), 所以它的反操作也是replace(), 只不过把add和remove的东西交换一下.
关于replace()和show(), hide()的选择, 要根据实际使用情形来定.
replace()的好处是会减少内存占用, 但是返回时需要重新走完初始化的过程.
show()和hide()只是控制了fragment的显示和隐藏, 不会改变生命周期状态, 也即fragment始终是处于running状态的, 被保持在内存中, 适用于频繁切换的情形.
remove(), replace()是否加到back stack对生命周期的影响
前面说过, replace() == remove() + add()
新的fragment将取代在容器布局中的fragment, 如果没有,将直接添加新的fragment.
是否添加到back stack对fragment的生命周期是有影响的.
remove()或者replace()的时候,如果commit()之前没有调用addToBackStack(),那个旧fragment将会被destroyed和detach; 即完全销毁和移除.
如果调用了addToBackStack(),旧的fragment会处在stopped状态,调用到onDestroyView(), 可以通过返回键来resume.
这个时候对于旧的Fragment来说, 成员变量依然在,但是View被销毁了. 所以返回时它的生命周期从onCreateView()开始重建View.
Fragment的坑
嵌套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来说, 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()是下一级别管理者.
这实际上是一个树形管理结构.
嵌套Fragment使用常见错误
错误情形1: 把嵌套Fragment放在布局里
把嵌套Fragment放在布局里 -> InflateException in Binary XML
看起来嵌套fragment的使用除了要用getChildFragmentManager()以外, 其他跟之前似乎没什么区别.
如果嵌套的fragment不需要太多控制, 固定地占据了一块地方, 你可能想当然地为了省事就把它放进了xml布局文件里, 写个标签.
运行一下初看起来似乎没什么错, run一下也能显示出来, 但是千万不要这样做, 多玩两下更复杂的你就知道了.
上面官网介绍时就有这么一句:
人家这么说肯定是有原因的哇, 下面我来告诉你我知道的问题:
如果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的时候.
打印具体的异常栈信息可以看到:
Solution 1: 动态添加child fragment
解决上面的问题有各种方法, 最常规的做法是, 使用动态添加:
Solution 2: 在异常之前remove child fragment
如果你的子fragment非要加在布局里不可, 而你的程序确实会有重建父fragment view的情形.
为了避免上面的异常, 你也可以这样做(tricky and not recommended):
|
|
在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, 于是就会抛出异常.
在Fragment F中, 先添加一个FrameLayout, 再把child fragment A加进去.
然后在Activity中, 用D replace F, 按back键返回, 就会有crash.
这是因为返回的时候FragmentManager找不到对应的container了.
所以应该避免这种做法, 尽量把fragment加进parent的根布局里, 而不是某个动态添加的布局.