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
调用之前,会先执行已提交但尚未执行的事务,因此这些事务的状态会正确保存,也不会触发异常。