相比其它大多数 Android API,Fragments 近几年的变化要更大一些。最初它作为 Android 平台的一部分,后来成为 Android Support Library 的一部分,现在又以 AndroidX Fragments 的形式独立成为了 Jetpack 的一部分。
提示 : 您不应该再需要使用 Android 框架里的 Fragment。除了它会在 Android 10 中被弃用以外,在弃用之前的这段漫长的时间里尘封于框架中,不会有任何更新和漏洞修复,同时也不会针对旧型号的设备或者旧版本的系统进行兼容性适配。
Android 架构组件 已经接管了 Fragment 大量的传统职能 (比如使用 LifecycleObserver 来监听生命周期的回调或者使用 ViewModel 来保持状态)。如果您使用 Fragment,就是通过 FragmentManager 来进行添加、移除和交互操作。
在 Fragment 1.3.0-alpha08 版本中 (最新版本 1.3.0-rc01),已经完成了针对 FragmentManager 内部大量的重构工作。该版本通过更轻量的、测试性更强以及更加易于维护的内部类替代了大量原来在 FragmentManager 内实现的逻辑,该内部类的核心是 FragmentStateManager。
提示 : 本文中我会谈论很多关于 FragmentManager 的内部原理。简而言之: 请特别针对 Fragment 1.3.0-alpha08 进行回归测试,并且在发现任何回归现象后在 Issue Tracker 这里告诉我们。
新的状态管理器负责很多 Fragment 的关键环节:
- 在生命周期方法中移动 Fragment
- 添加动画和切换效果
- 处理推迟后的事务
我们从底层分析了原本系统的实现机制,发现有一些 问题,所以重写了状态管理器。我们解决了十几个已有的问题。经过重构,在单独的 FragmentManager 中支持多个返回堆栈了,并且还 简化了 Fragment 的生命周期。
FragmentManager 的 moveToState() 方法
每个 FragmentManager 都关联着一个宿主 (host)。在绝大多数 fragment 的用法中,FragmentActivity 是最突出的一个 (当然也有涵盖整个层次的 FragmentController 和 FragmentHostCallback 可用于构建自定义的宿主,这里我们先不讨论它们)。随着 Activity 的生命周期在 CREATED、STARTED 和 RESUMED 状态中的转移,FragmentManager 也会相应的把这些状态的改变传递到它的 Fragment 中。也就是 moveToState() 所发挥的作用。
当然了,事情并没有这么简单直接。有很多条件逻辑可以控制 fragment 真正所处的状态,Activity 的生命周期状态 (或者对于嵌套的 Fragment 父级所处的状态) 仅仅是第一步,它可以作为 Fragment 所处状态的上限标准。这里的上限标准可以保证 Activity、Fragment 和它们的子级 Fragment 之间保持合理的嵌套关系。
所以我们在 简化 moveToState() 方法 的首要的工作就是将上述逻辑汇总到一个地方,所以就诞生了 FragmentStateManager。每个 Fragment 实例都在底层与一个 FragmentStateManager 相关联。通过在内部使用这个类,我们可以从 FragmentManager 里去掉大量与 Fragment 交互的代码 (比如调用 Fragment 的 onCreateView 方法和其它与生命周期相关的方法)。
这样的代码分离还可以让我们通过一个单独的方法解决向前兼容所需的逻辑,即 Fragment 应该处于何种状态,然后将其汇总到一个地方: computeExpectedState()。该方法追踪所有当前的状态并且决定 Fragment 应该处于哪个状态。虽然 98% 的时间里,Fragment 和它的宿主或父级 Fragment 所处的状态相同,但是剩下的 2% 所发生的变化对基于 Fragment 的应用影响深远。
然而,有一种情况下我们没有办法确定 Fragment 的实际状态: 延迟加载的 Fragment。
延迟加载的 Fragment
Fragment,无论好与坏,都从 Activity 上继承了大量相同的命名规则和 API 调用接口。其中一部分继承是关于界面切换和在应用准备好之前推迟切换操作。该逻辑对于涉及到共享元素切换的应用场景非常重要 (有时您希望在场景切换之前就知道将要加载的图片分辨率和在屏幕上的位置),同时也保障了在界面切换的过程中不会触发大量的加载操作。
延迟加载的 Fragment 拥有两大重要特质:
- 视图虽然被创建了,但是不可见;
- 生命周期的上限为 STARTED 状态。
当您调用 startPostponedEnterTransition() 的时候,fragment 的切换操作就开始了,视图会变为可见,Fragment 的状态会变为 RESUMED。而上述这些是由新的状态管理器实现的,之前的 Fragment 并不是这样的机制。作为参考,我们这里引用一个相关的 问题描述:
当 Fragment 使用
postponeEnterTransition()
方法实现延迟加载的时候,所期望的效果是添加了 Fragment 的容器,在 Fragment 调用startPostponedEnterTransition()
之前,不运行任何进入界面的动画或者之前已经在队列里的退出动画 (比如replace()
操作)。另外一个预期的效果是当容器推迟加载的时候,Fragment 不会进入 RESUMED 状态。
然而,FragmentManager 似乎并没有按照这个过程操作,而是将 Fragment 和整个 FragmentManager 置于一个奇怪的、不一致的状态。
换而言之,任何与当前被延迟加载的 Fragment 相关的 FragmentTransaction 都会被回退到之前的状态 (比如返回到上一状态),但是这些 Fragment 并没有转换为合适的状态。
这样就导致了一系列问题:
- Fragment 的视图创建了,但是 Fragment 却没有被添加 (isAdded() 会返回 false)
- findFragmentById() 不会返回刚刚添加的 Fragment,即使调用 commitNow() 也不行
- 当 FragmentManager 启动后,Fragment 在一个中间状态卡住而不会跟随启动 (https://issuetracker.google.com/issues/129035555)
- FragmentTransactions 的执行顺序会被打乱 (https://issuetracker.google.com/issues/147297731)
- 容器的其它动画仍然会播放 (比如已经开始播放的弹出动画) (https://issuetracker.google.com/issues/37140383)
- onCreateView() 会被调用第二次 (https://issuetracker.google.com/issues/143915710)
事实上解决上述的任意问题都需要将整个延迟加载 Fragment 所用到的回退处理过程替换掉,使用一套系统保持 FragmentManager 处于一致的、最新的状态,同时又能保留延迟加载 Fragment 的一些重要的特性。
在容器层面进行操作
FragmentManager 包含一个好用的属性,您可以将 Fragment 所处容器的 ID 传递给该属性。甚至对于一个单独的 FragmentTransaction,您可以添加 Fragment 到容器,从另一个不同的容器中移除另外的 Fragment,替换第三个容器最上层的 Fragment 等等。操作的相互交错仅仅出现在 Fragment 动画切入、切出的时候,这一切都只会在容器层面发生。
Fragment 支持多个动画系统:
- 旧版的已经 没什么实际作用 的 Animation API
- 开发框架里的 Animator API
- 开发框架里的 Transition API (仅仅支持 API 21 及以上,同样没什么作用了)
- AndroidX Transition API
您应该也知道,命名是计算机科学中的一大难题,所以当我们准备构建一个类去控制所有这些 API 的时候,我们费了一些功夫才决定将它命名为 SpecialEffectsController (该类不属于公共 API,所以未来还可以修改名称)。该类存在于容器层面,协调所有与 fragment 切入切出相关的 “特殊效果”。
SpecialEffectsController 是决定容器未来变化的唯一源头。换而言之,如果最先添加的 fragment 被延后加载了,整个容器都会被延后加载。再也不需要在 FragmentManager 层面实现其它逻辑,或者任何回退操作 (我们提到过它可以影响多个容器)。因此,FragmentManager 处于正确的状态,并且我们还能够获得所有延迟加载的 Fragment 的特殊属性。
这个基础的 API 可以让我们将所有酷炫效果的 API 集中到单独的 DefaultSpecialEffectsController 中,它负责实现切换效果和动画效果以及 Animator。也就是说将分散在 FragmentManager 中的逻辑集中到一个地方。
“新的状态管理器” 意味着什么
其实它的意思是说将下面这个结构:
旧的状态管理器: 所有的逻辑都包含在 FragmentManager
替换为下面这样的结构:
新的状态管理器: FragmentManager 与独立的 FragmentStateManager 实例进行交互,然后 FragmentStateManager 再通过容器中的 SpecialEffectsController 协调其它 Fragment
通过分离 FragmentManager,整体逻辑已经在各个层次进行了大幅简化:
- FragmentManager 仅仅包含用于所有 fragment 的状态
- FragmentStateManager 在 fragment 层面管理状态
- SpecialEffectsController 在容器层面管理状态
职责分离的设计结构使我们扩展了 30% 的测试用例,覆盖了更多的应用场景,这些场景很多在相互孤立的状态下几乎无法测试。
会有行为变更需要处理吗?
不会。事实上,我们在旧的和新的状态管理器之间运行了大量的 fragment 内部测试,以保证我们完成足够数量的回归测试。
您可以在 版本发布日志 中找到和新的状态管理器相关的 bug 修复列表。所以可以看一下该列表,确保您的问题不是由于之前错误的处理方式所造成的,同时也可以移除之前有问题的逻辑代码。
和 Fragment 1.2.0 中的 onDestroyView 的更新相类似,新的状态管理器会在您的 fragment 的切换/动画/animator/特效结束之前始终保持在 STARTED 状态,然后无论是直接进行延迟加载还是间接延迟加载,所有的 fragment 状态都保持一致,这是因为它们属于相同的容器。
如果发生行为变更,怎么办?
当您升级到 Fragment 1.3.0-alpha08 后,新的状态管理器是默认开启的。如果您发现了应用效果发生了变化,首先可以通过下面新增的实验性 API 测试该变化是否是由于新的状态管理器造成的:
FragmentManager.enableNewStateManager(false)
这个 API 是可以帮助您禁用新的状态管理器,以帮助您检查当前的变化是否和它相关。它帮助您扫清了升级到 Fragment 1.3.0-alpha08 的障碍,如果有任何问题,请在这个 Issue Tracker 中提交。
提示 : FragmentManager.enableNewStateManager() API 是实验性质的。也就是说它并不包含在 Fragment 的稳定 API 中,并且可能在未来被移除。移除旧的代码是代码量降低的重要步骤,但是为了能让整个过程无误且顺利,我们准备在 Fragment 1.3.0 稳定版本发布之前都不会移除该 API。也许可以考虑在 Fragment 1.3.1 发布的时候移除该 API 的相关调用代码。
通过长达 11 个月的 100 多个独立修改,造就了 Fragment 最大的一次内部升级,并且为我们带来了可维护性更高,可持续性更好以及更易理解的基础代码。这意味着 Fragment 的一致性更高,以及对您来说可以依赖更加稳固的基础代码来构建应用。我们也非常欢迎大家积极 提交问题 和反馈,一起参与到新的状态管理器的优化工作中来,使它变得更加完善。