Fragment视图内嵌套Fragment时,使用getChildFragmentManager()而非getActivity().getSupportFragmentManager()- 创建
Fragment时,先尝试复用已有的Fragment - 不可以在构造工厂方法内为
Fragment注入 Bundle Arguments 以外的依赖 - 提交
FragmentTransaction时,优先使用commit()而非commitNow() - 考虑使用
commit()前检查状态,而非无脑使用commitAllowingStateLoss() - FAQ
Fragment 视图内嵌套 Fragment 时,使用 getChildFragmentManager() 而非 getActivity().getSupportFragmentManager()
如果子 Fragment 视图是在某个 Fragment 视图内创建的,那么应该使用该 Fragment 的 ChildFragmentManager 管理其状态。
FragmentManager 不仅负责 Fragment 状态切换,还负责在重建 Activity 时恢复 Fragment 状态。在恢复 Fragment 状态时,如果使用了错误的 FragmentManager,可能会导致 Fragment 状态恢复错误,引起界面异常、崩溃或逻辑错误。
考虑以下错误情形:
- 使用视图容器 id 添加
SubFragment到MainFragment的子视图,但使用了MainFragment对应Activity的FragmentManager - 对应
Activity动态添加了有相同 id 的视图到视图树(例如ViewPager) - 恢复
Fragment状态时,从视图树找到了动态添加的视图,而非先前作为容器的视图,造成界面异常或崩溃。
此外,Lifecycle 相关 API 支持锁定 Fragment 的 maxLifeCycle,此设置会应用到其 ChildFragmentManager 管理的子 Fragment。如果使用了错误的 FragmentManager,会导致生命周期表现不符合预期,造成意外的 LiveData 更新、业务逻辑错误等问题。
创建 Fragment 时,先尝试复用已有的 Fragment
由于 FragmentManager 保存了 Fragment 的状态,在配置变更造成 Activity 和 Fragment 重建时,会先从保存的状态创建 Fragment,并试图关联到对应的视图层级。
如果不加判断地创建和添加 Fragment,轻则丢失保存的状态,重则重复添加 Fragment 导致 crash。
如果实际添加到视图的 Fragment 实例是由系统恢复的,而业务逻辑依赖的 Fragment 实例是自己创建的,则可能造成难以复现和排查的问题。
为此,在创建 Fragment 时,尝试复用已有的 Fragment。如果判断已有的 Fragment 不能复用,要显式移除该 Fragment,避免内存泄露。
1 | // 错误示范 |
androidx.fragment.app 1.2.0 以上的版本引入了基于类的 add() 和 replace() 方法,推荐使用这些方法替代显式创建新实例的方式。
1 | FragmentManager fm = fragment.getChildFragmentManager(); |
不可以在构造工厂方法内为 Fragment 注入 Bundle Arguments 以外的依赖
由于 Fragment 重建机制的存在,在构造工厂方法里注入 Bundle Arguments 以外的依赖是不可靠的:Fragment 重建时,只会恢复保存的 Bundle Arguments,并不会恢复其他依赖。
虽然可以对这种情况判断并单独注入依赖,但相对更好的方式是在 FragmentLifecycleCallbacks 统一处理。
1 | // 不推荐的做法 |
1 | // 使用 FragmentLifecycleCallbacks 注入依赖 |
也可以使用 Hilt 之类的依赖注入框架,这样开发者不必自己考虑如何注入依赖的问题。
最后,建议使用 androidx.fragment.app 1.2.0 以上版本引入的基于类的 add() 和 replace() 方法,逐步弃用传入 Fragment 实例的 add() 和 replace() ,最终完全弃用 Fragment 构造工厂方法,以避免潜在风险。
提交 FragmentTransaction 时,优先使用 commit() 而非 commitNow()
使用 commit() 而非 commitNow() 有助于 FragmentManager 合并执行多个 FragmentTransaction,重排其内部操作的顺序、消除冗余操作。
考虑使用 commit() 前检查状态,而非无脑使用 commitAllowingStateLoss()
开发过程中不时遇到使用 commit() 时抛出 IllegalStateException 异常,为应对该异常,许多开发者倾向于使用 try-catch 捕获,或使用 commitAllowingStateLoss() 代替 commit()。但这样做是有其副作用的,最好是理解框架抛异常的原因,并针对性地应对。
commit() 时抛出 IllegalStateException 异常的原因有以下几种:
- 当前
FragmentManager已经保存了Fragment状态(宿主调用了onSaveInstanceState),并且还没有重新恢复(还没有调用onCreate/onStart/onResume),因此此后的提交操作不会记录到保存的状态中,因此也不会在重建时恢复; - 当前
FragmentManager对应的LoaderManager已经完成了加载或正在重置,在onLoadFinished或onLoaderReset回调返回前,不允许执行commit,因为官方认为这个回调可能在任何时间返回(包括Fragment状态已保存或已被销毁),因此并不安全。(我个人认为这属于设计失误,用户应该自己确保调用安全)
对于 Fragment 状态保存后抛出异常,其实是框架的预期行为,旨在提醒开发者确认并正确处理方法调用时机。
考虑以下使用场景:
- 在网络数据返回时,创建并展示
Fragment,更新某个LiveData状态值,表示该Fragment处于展示状态; - 如果此时用户实际上已经离开 app,
onSaveInstanceState已经调用,此时直接调用commit()添加了该Fragment,应用将抛出异常; - 如果此时改为使用
commitAllowingStateLoss()添加了该Fragment,将不会抛出异常。- 如果用户返回 app 时,app 没有被回收,页面没有被重建,表现也符合预期:
Fragment正常展示,LiveData的值正常更新; - 但如果用户返回 app 时,页面经历了重建过程,将出现不符合预期的表现:
Fragment没有正常展示,但LiveData的值被更新了,导致状态不一致,进而引起功能逻辑异常。这样的问题由于偶发性,也比较难以排查。
- 如果用户返回 app 时,app 没有被回收,页面没有被重建,表现也符合预期:
因此,调用 commit() 时抛出异常,是框架在提醒开发者,Fragment 状态的变更不会在重建之后恢复,当页面重建时可能会出现不一致的问题。对此开发者有两种应对方式:
-
事前预防:尽量不要在启动生命周期回调(
onCreate/onStart/onResume)之外,直接调用FragmentTransaction.commit()。
在需要在用户点击时、网络数据返回等异步回调中展示Fragment的场景,通过设置LiveData,将调用延迟到合适的时机。如果不想或不适合使用
LiveData,则在调用commit()前,需要使用FragmentManager#isStateSaved()检查状态是否已经被保存:如果状态尚未被保存,则调用commit()提交变更;如果状态已经被保存,则不调用commit()提交变更,并自行处理恢复逻辑。 -
事后处理:使用
commitAllowingStateLoss()方法。
这意味着:开发者充分了解Fragment状态丢失的可能结果,并确认该情况不影响业务流程,或针对该情况调整了业务逻辑。
一种处理方案是:在处理Fragment状态变更前,使用ViewModel + LiveData记录将要进行的操作:在使用与Fragment状态相关的逻辑前,检查Fragment状态是否符合预期:如果不符合预期,则要根据之前的记录,进行必要的操作,以恢复预期的状态。
这种方案的维护成本较高,容易出错,因为往往不能确定在业务逻辑的何处会判断Fragment的状态,因此不推荐使用。
对于 LoaderManager 引起的问题,Android 官方文档已经废弃 LoaderManager,建议是在新代码中不再使用 LoaderManager ,并尽快将已有的相关业务逻辑迁移到使用 ViewModel + LiveData 开发。
1 | // 不推荐在回调用 commitAllowingStateLoss |
1 | // 推荐:使用 LiveData 推送变更,系统会在合适的时机回调 |
FAQ
使用 commit() 提交事务时,Fragment 状态变更是异步进行的。如果调用 commit() 方法前尚未保存状态(因此可以提交),但在事务真正执行前,插入了 onSaveInstanceState 调用,是否会触发异常?
答:不会。在 onSaveInstanceState 调用之前,会先执行已提交但尚未执行的事务,因此这些事务的状态会正确保存,也不会触发异常。