【译】Fragment 的重大重构 —— 介绍 Fragment 新的状态管理器

7,014 阅读11分钟

原文:Fragments: Rebuilding the Internals. Introducing: the new state manager
作者:Ian Lake
译者:Flywith24

多年以来,Fragment 要比大多数 Android API 更新得更多。它们最初是 Android platform 的一部分,后来成为 Android Support Library 的一部分,现在以 AndroidX Fragments 的形式成为了 Jetpack 组件的一部分。

注意:您绝不应该使用 Android framework 版本的 Fragment。不仅因为它已经在 Android 10 中完全弃用了,还因为它已经长时间没有被修复 bug,而且保证不同设备和 API 级别之间的一致性。

虽然 Architecture Components 接管了很多过去需要使用 Fragment 的场景(例如 使用 LifecycleObserver 处理生命周期的回调,或者使用 ViewModel 来保留状态),如果您使用 Fragment 来处理这些场景,则需要通过 FragmentManager 来进行 add / remove 操作来与之交互。

随着 Fragment 1.3.0-alpha08 的发布,FragmentManager 内部的重大重构已经完成。该版本使用 更小的,可测试的,可维护的(内部)类替换了许多 FragmentManager 中的逻辑。其核心类是 FragmentStateManager

注意:我将在这篇文章中讨论 FragmentManager 的内部原理。如果在使用 1.3.0-alpha08 遇到任何问题,请尽快提交 file issues 协助我们修复。

新的 state manager 的职责:

  • 通过 Fragment 的生命周期方法来移动它们
  • 运行转场动画
  • 处理延迟事务

我们彻底回顾了这些系统过去的工作方式,发现 它们需要被从头重写 ,于是我们重写了它们。它们比以往的任何时候都更好,我们能够关闭至少 10 个长期存在的相关 issues,并且这个内部重构为单 FragmentManager 支持 多返回栈 扫清了道路(译者注:Bottom Navigation 管理平级界面的问题),并简化了 Fragment 的生命周期。

FragmentManager moveToState()

每个 FragmentManager 都与一个 host 关联,对于大多数 fragment,host 为 FragmentActivity(使用 FragmentControllerFragmentHostCallback 可以自定义 host,但不在本文的讨论范围)。当 activity 转移到 CREATEDSTARTED,以及RESUMED, FragmentManager 派发这些更改到它的 Fragments。这是 moveToState() 的职责。(译者注:源码分析参见 【背上Jetpack】从源码角度看 Fragment 生命周期)。

当然,它没有那么简单。有很多条件逻辑来确定 fragment 该处于什么状态—— activity 的生命周期状态(如果是嵌套 Fragment 则是其 parent fragment 的生命周期状态)只是第一部分,它被称为 fragment 能够处于的 max state。最大状态保证了 activity ,fragment,以及它们的 child fragment 能够正确地嵌套。

因此 简化 moveToState() 的首要任务是将所有逻辑抽离到一个位置。于是 FragmentStateManager 诞生了。每个 fragment 实例与一个 FragmentStateManager 绑定。通过引入这个内部类,我们能够从 FragmentManager 中抽取与 fragment 交互的大量代码(例如调用 fragment 的 onCreateView 方法以及其他生命周期方法)。

该拆分还使我们能够编写一个方法,该方法将使用所有向后兼容的所需逻辑来确定 fragment 实际应处于的状态,并将其集中在一个位置:computeExpectedState()。该方法追踪当前的所有状态并且确定 fragment 应处于什么状态。98% 的时间,他们与 host / parent fragment 处于同一状态,但是剩下的 2% 与那些建立在 fragment 上的 app 有很大不同。

但是有一种我们无法确定正确状态的情况:postponed fragments。

Postponed fragments

无论是好是坏,Fragment 都继承了许多与 Activity 相同的命名的 API。这种继承的一部分是围绕转换以及推迟转场直到目标准备好的能力。这对共享元素转场非常重要,与此同时还要确保在转场的同时不会产生更密集的数据加载(译者注:为了转场时先完成动画,再完成大数据的渲染)。

延迟 fragment 拥有两个重要的特性:

  1. 它的 view 已被创建,但不可见
  2. 它的生命周期上限为 STARTED

当您调用 startPostponedEnterTransition() 后,fragment 的转场便会执行,view 将变得可见,并且 fragment 将会移动到 RESUMED。实际上,这正是新的 state manager 做的,过去的 fragment 不是这样工作的,详情参考 Postponed Fragments leave the FragmentsFragmentManager in an inconsistent state 这两个bug。

当 fragment 被使用 postponeEnterTransition() 推迟了,预期的行为是:fragment 被添加到的 container 不会运行任何进入动画或者之前排队的退出动画(例如 replace 操作)直到 Fragment 调用 startPostponedEnterTransition()。同时当 fragment 的 container 被延迟时,fragment 不会达到 RESUMED 状态。

然而,似乎 FragmentManager 没有执行上述操作,反而将 Fragment 和整个 FragmentManager 转移至一个怪异的,不一致的状态。

也就是说,任何与 postponed Fragment 的 container 相关的 FragmentTransaction 被「回滚」(如返回上一 fragment),但实际上,这些 fragment 没有移至其正确的状态。

这导致了一些列的问题;

实际上解决这些问题中的任何一个都意味着需要一个系统替换 postponed fragment 使用的整个回滚过程,该系统保证 FragmentManager 的一致性,最新状态,同时保留 postponed fragment 的主要特性。

在 container 层工作

FragmentManager 具有 container 这个不错的属性(读起来:很方便,但作为维护者却不那么有趣),在该属性中,您可以为要放置 Fragment 传入任何 container id 。甚至在一个 FragmentTransaction 中,您可以 add 一个 fragment 到一个 container,从一个不同的 container remove 另一个,replace 第三个container 最顶端的 fragment,等等。

fragment 动画进/出会产生接触,这仅发生在 container 层。

Fragment 支持一些列的动画系统:

  • 老旧破烂的 framework Animation API
  • framework Animator API
  • framework Transition API(仅支持 21+,同样很烂)
  • AndroidX Transition API

众所周知,命名是计算机科学中最难的问题之一,因此当我们去构建一个可以控制所有这些 API 的类时,花了一些时间才决定使用 SpecialEffectsController(该类不是 public API,因此命名可能更改)。该类存在于 container 层,并且协调进入和退出 fragment 相关的所有特效("special effects") 。

SpecialEffectsController 是 container 中真实情况的 唯一信源。这意味着如果最顶部被 add 的 fragment 被推迟了,整个 container 也将被推迟。不再需要 FragmentManager 层的逻辑,也不需要 transaction 的任何回滚(如我们前文提到的,它将影响多个 container)。因此, FragmentManager 处于正确的状态,我们仍可以获得被延迟 fragment 的所有属性。

这个 API 还允许我们将 fragment 的所有疯狂的特效 API 集中到一个 DefaultSpecialEffectsController 中,该 controller 负责运行 transition,animation,以及 animator。再次过去分散在 FragmentManager 中的逻辑移动到了一个地方。

所以 'new state manager' 是个啥

它意味着要替代这种架构:

旧的 state manager:所有逻辑都在 FragmentManager

看起来更像这样:

新的 state manager:FragmentManager 与各个 FragmentStateManager实例进行通信,这些实例通过 SpecialEffectsController 与 container 中的其它 fragment 进行协调。

通过拆分 FragmentManager 的内部结构,每一层的逻辑都得到了极大简化:

  • FragmentManager 仅具有适用于所有 fragment 的状态
  • FragmentStateManager 在 fragment 层管理状态
  • SpecialEffectsController 在 container 层 管理状态

职责的分离使我们的测试套件扩大了近 30%,涵盖了几乎无法单独测试的更多场景。

我应该看到行为变化吗?

不。事实上,我们对新旧状态管理者都进行了很大一部分的 fragment 测试,专门用于确保我们有一套强大的回归测试。

但是,如果您依赖于不一致的状态,则可以将 FragmentManager 放入延迟的 fragment 中,然后,是的,您会发现实际上已经获得了正确的状态。 在 发行说明 中,您会找到与新状态管理器相关的错误修复列表,因此请仔细阅读以确保您的问题不是由您自己的解决方法引起的,而该解决方法是您可以现在删除的旧行为 。

与 Fragment 1.2.0 中的 onDestroyView 计时更改 类似,新的状态管理器将使 fragment 保持在 STARTED 状态,直到其 transitions/animations/animators/special effects 全部完成为止,从而使所有 fragment 保持一致,无论它们是否是直接 postponed 或由于同一 container 中的其它 fragment 引起的 postponed 。

如果我真的看到了行为变化?

新的状态管理器会默认开启。如果你在你的 app 中看到了与之前不同的行为,首先使用以下新的实验性的 API 来排查是否是由于新的状态管理器导致的。

当你更新了 Fragment 1.3.0-alpha08 后,新的状态管理器会默认开启。如果你在你的 app 中看到了与之前不同的行为,首先使用以下新的实验性的 API 来排查是否是由于新的状态管理器导致的。

我国启新的状态管理器。如果您发现应用程序和过去存在差异,首先可以使用新的实验性的 API 来排查是否与新的状态管理器相关:

FragmentManager.enableNewStateManager(false)

该 API 可以让您重温旧世界,让您验证所看到的的任何与之前不一致的改变是否由于新的状态管理器导致。如果确认是由于新的状态管理器导致,您可以构建一个示例项目重现您遇到的问题,并 在此提 Issue

注意:FragmentManager.enableNewStateManager() API实验性的

这意味着它不被视为 Fragment 稳定API 的一部分,可以随时删除。 删除所有旧代码可以节省大量代码,但是鉴于正确处理代码的重要性,我们可能要等到 Fragment 1.3.0 的稳定版本发布后才能删除 API,比如考虑在 fragment 1.3.1 发布时移除。

在 11 个月内进行了 100 多次个人更改,这绝对是一段时间内 Fragment 的最大内部更改,它使我们建立了更加可维护,可持续和可理解的代码。 这意味着跨 Fragment 的行为更加一致,并且它成为您在构建应用程序时可以依靠的坚实基础。 我们非常感谢您继续提出问题并 提供反馈

感谢 Jeremy Woods,Chet Haase,和 Nick Butcher。

译文完。

译者总结

Fragments: Past, Present, and Future (Android Dev Summit '19) 演讲中 Ian Lake 阐述了 fragment 的未来。掘金官方文章在这

  • Fragment 的通信问题
  • 多返回栈
  • 简化 Fragment 生命周期

其中 弃用 targetFragment API 使用新的 FragmentResult API 已经发布:详情移步【Jetpack更新之Fragment】1.3.0-alpha04 来袭,Fragment 间通信的新姿势

Fragment 的生命周期正在被简化,例如 onActivityCreated 被弃用了,详情移步【Jetpack更新之Fragment】终于动手了,onActivityCreated 被弃用。不过距离合并 Fragment 自身与其内部 View 的生命周期还有很长一段路(不知官方是否会放弃)。

多返回栈一直被本文解决的问题阻塞,文中所说的过去 11 个月便是去年 9 月 Android Dev Summit '19 到现在。而下面的 issue 是2018 年 5 月创建的。

好在多返回栈的绊脚石已经解决,不过这里我们也可以看到 SDK 级别代码难以维护后的优化成本,文中重写了 fragment 状态管理逻辑,即使做了大量测试,仍要保留旧版逻辑并留出切换的入口。

查看 Fragment 代码变化我们能很明显的认识到「职能单一」重要性,这对构建可维护的,可理解的,可测试的,健壮的代码十分重要。

关于 fragment 的其他内容,可参考