Fragment 的过去、如今和未来

Fragment 是 Android 中历史十分悠久的一个组件,它在 API 11 被加入,时至今日已成为 Android 开发中最经常使用的组件之一。Fragment 有了哪些新特性、修复了哪些问题,都是开发者们十分关心的话题。下面咱们就来从新说一说 Fragment —— 不只仅是说如今的 Fragment,还会回顾它的发展,并让您一瞥它将来的样子。java

Fragment 的诞生与发展

不知道您是否还记得 "上古时期",在那些尚未 Fragment 的日子,几乎全部逻辑都被放在了 Activity 中,使得 Activity 臃肿而又混乱。此时,Fragment 做为一个微型 Activity 而诞生,迈出了缩减 Acitvity 之路上的一小步。android

不过 "欲戴王冠,必承其重",Fragment 由此继承了诸多原本是为 Activity 设计的 API 和组件。其中有些组件,其实应该被设计为独立的 View,好比当年的 Action Bar,这个组件如今已经被 Toolbar 代替了;又好比现今已经基本没人使用的 Context Menus。API 这部分就更复杂一些,全部之前要发送到 Activity 的信息,如今也要发送到 Fragment,咱们处理权限时很经常使用的 onActivityResult 就是这种状况下的产物;当 Android 加入运行时权限时,Fragment 理所固然的也要支持,由于 Activity 已经支持了。相似的 API 还有 onMultiWindowModeChanged 以及 onPictureInPictureModeChanged。架构

遵循着成为一个微型 Acitivity 的设计初衷,Fragment 天然而然的就获得了这些功能。可是回过头来看,这些功能其实并非专门为 Fragment 设计的 —— 随便一个什么东西,有了这些回调,彷佛都能胜任 Fragment 的功能。这种情况使得咱们开始转变思路,并尝试抛弃让 Fragment 去作微型 Activity 的想法,现在的 Fragment 正是由此而来。app

Fragment 的现状

咱们的想法产生改变,仍是 2011 年的事,到今天已经经历了很长的时间。这期间咱们花费了不少的精力去从新构思 Fragment 的定位。咱们但愿 Fragment 成为一个真正的核心组件,它应该拥有可预测的、合理的行为,不该该出现随机错误,也不该该破坏现有的功能。框架

其实咱们但愿挑个时间发布 Fragment 的 2.0 版,它将只包含那些新的、好用的 API。但在时机成熟以前,咱们会在现有的 Fragment 中逐步加入新的并弃用旧的 API,并为旧功能提供更好的替代方案。当没人再使用已弃用的 API 时,迁移到 Fragment 2.0 就会变得很容易。接下来我就来说讲,咱们为此所作的一些工做。ide

FragmentScenario

首先我要说,合理的 API 应当是可测试的。不能被测试的代码不是好代码,如今已经 2020 年了,咱们也但愿 Fragment 能在这方面作得更好。因而,经过与 AndroidX 团队紧密合做,咱们开发出了测试工具 FragmentScenario,它能够用来单独对 Fragment 进行测试。FragmentScenario 基于 ActivityScenario 实现,这也意味着它一样适用于 Instrumentation 和 Robolectric 测试。同时它的 API 十分简洁,它最主要的方法就是 onFragment,这个方法接收一个 Lambda 表达式,而 Lambda 表达式则在其中返回已存在的 Fragment 实例。同时 FragmentScenario 也提供了方便测试生命周期和重建 Fragment 的 Hook 方法。工具

以下示例代码来讲,首先使用 launchFragmentInContainer<MyFragment>() 建立 FragmentScenario 对象,这一步操做将会帮您完成建立 Fragment 的整个流程。接下来就能够进行测试了,您能够看到,使用 onView 测试 click() 方法时,Fragment 的层级结构已经被加载完成。最后只要在 onFragment 中检查 Fragment 的状态,就能够确认 Fragment 是否有正确处理点击事件。布局

@Test
fun testEventFragment() {
    val scenario = launchFragmentInContainer<MyFragment>()
    onView(withId(R.id.refresh)).perform(click())
    scenario.onFragment { fragment ->
        // 检查 Fragment 有没有正确处理点击事件   
    }
}

若是须要测试一些更加复杂的状况,好比 Fragment 的生命周期切换,您能够调用 Scenario 的 moveToState() 方法,来让 Fragment 触发各类生命周期。测试 Fragment 的重建也是相似操做,假如您想要测试是否正确存储和恢复了 Fragment 的状态信息,只须要调用 recreate() 方法,就能够检查 Fragment 重建先后状态信息的保存状况,就是这么简单。测试

@Test
fun testEventMoveToCreatedFragment() {
    val scenario = launchFragmentInContainer<MyFragment>()
    scenario.moveToState(State.CREATED)
}

@Test
fun testEventFragment() {
    val scenario = launchFragmentInContainer<MyFragment>()
    scenario.recreate()
}

FragmentFactory

讲到 Fragment 的重建,就联想到 Fragment 的实例化。Fragment 已经有不少种实例化方式了,后来又有了 FragmentScenario。咱们但愿能统一这些方法,而解决方案即是 FragmentFactory,它让咱们能够注入 Fragment 的构造方法,也顺带解除了 Fragment 必须有一个无参构造方法的限制。动画

下面是一个简单的 FragmentFactory,它只有一个方法 —— instantiate,您只须要在这个方法中传入 Fragment 的类名,随后 super.instantiate() 方法就会使用反射调用对应 Fragment 的无参构造方法。正如咱们在 《Android 依赖注入指南》 这场演讲中提到的,咱们很乐意经过这种模式来减小使用者的重复工做。而若是您须要传入参数,则能够将参数传入 FragmentFactory 并经过构造方法注入将参数传入 Fragment。

接下来,您须要将 FragmentManager 的 FragmentFactory 设置为您的 FragmentFactory 。这一步最好放在 super.onCreate() 以前,由于它是从新实例化 Fragment 的地方。

private class MyFactory() : FragmentFactory() {
    override fun instantiate(
        classLoader: ClassLoader,
        className: String
    ) = when (className) {
        MyFragment::class.java.name -> MyFragment()
        else -> super.instantiate(classLoader, className)
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    supportFragmentManager.fragmentFactory = MyFactory()
    super.onCreate(savedInstanceState)
}

为了保证 API 的一致性,咱们还准备经过下面的方式统一其余地方建立 Fragment 的方式。好比 Commit 操做,咱们代理了您的 FragmentFactory,如今您只须要使用 Fragment 的类名,经过一行简单的代码,便能完成 Fragment 的建立、添加和初始化。

// 经过类名来添加 Fragment 
supportFragmentManager.commit {
    add<MyFragment>(R.id.container)
}

相似的,使用 FragmentScenario 时,只须要传入您的 FragmentFactory 便可。这个 FragmentFactory 既能够是只用来模拟依赖的虚拟 Factory,也能够是用于更多测试的真实 FragmentFactory。

// 使用自定义 FragmentFactory 建立 FragmentScenario
val scenario =
  launchFragmentInContainer<MyFragment>(factory = MockFactory())

FragmentContainerView

关于 API 的一致性,咱们也尝试解决了 Fragment 的另外一个一致性问题。

咱们发如今添加 Fragment 时,经过 <Fragment> 标签添加与经过 FragmentTransaction 使用的是彻底不一样的两套系统。为了提供行为一致的 API,咱们建立了 FragmentContainerView,并把它做为 Fragment 专属的容器。

FragmentContainerView 继承于 FrameLayout,但它只容许填充 FragmentView。它同时也替代了 <Fragment> 标签,只要在 class 属性中传入类名便可。因为 FragmentContainerView 内部使用的是 FragmentTransaction,因此无需担忧,稍后在替换这个 Fragment 时也不会出现问题。

<!-- 与在 onCreate 中调用 add() 方法效果相同 -->
<androidx.fragment.app.FragmentContainerView
    class="com.example.MyFragment"
    android:id="@+id/container"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

FragmentContainerView 也让咱们有机会解决一些动画问题。例如 Fragment 在 Z 轴的层级问题。以下图所示,咱们能够看到在 FrameLayout 中,Fragment 切换时没有显示动画,而是整个跳出到了屏幕上。这种问题是因为切入的 Fragment 和它的动画位于以前的 Fragment 的层级之下致使的。而 FragmentContainerView 会确保 Fragment 间的层级关系处于正确的状态,咱们就能够看到切换动画了。

OnBackPressedDispatcher

另外一个长期困扰咱们的问题,是在 Fragment 中处理系统回退事件。为了解决这个问题,咱们加入了 onBackPressedDispatcher。咱们没有选择在 Fragment 中添加这个 API,而是将其加入了 Activity 中。如今任何组件均可以经过依赖 Activity 来处理回退事件。

下面是一段使用 onBackPressedDispatcher 的示例代码。您能够看到,首先 Fragment 从调用它的 Activity 中获取 onBackPressedDispatcher 对象,而后经过 addCallBack() 方法建立了一个 OnBackPressCallback,因为 Fragment 是 LifecycleOwner,因此这里能够传入 "this"。在此示例中,若是用户触发了回退操做,就会弹出一个确认窗口,而若是用户随后表示不管如何都想要退出的话,您能够先使回调失效,而后就能够执行默认的回退操做。

val dispatcher by lazy { requireActivity().onBackPressedDispatcher }
lateinit var callback: OnBackPressedCallback

override fun onCreate(savedInstanceState: Bundle?) {
  super.onCreate(savedInstanceState)
  callback = dispatcher.addCallback(this) {
    showConfirmDialog()
  }
}

private fun onConfirm() {
  callback.enabled = false
  dispatcher.onBackPressed()
}

因此这里其实并无新的 API,只是整合了 Fragment 和架构组件现有功能。而咱们接下来也打算进一步加深与架构组件的整合。举个例子,在 Fragment 中理应能够方便地得到 ViewModel 实例,但现实的情况却稍微有些麻烦。为了解决这个问题,咱们建立了一些 Kotlin 属性代理。以下面的代码所示,利用这些属性代理,您能够轻松得到不一样做用域的 ViewModel。

// 让获取 ViewModel 实例变得简单
val viewModel : MyViewModel by viewModels()
val navGraphViewModel: MyViewModel by navGraphViewModels(R.id.main)
val activityViewModel: MyViewModel by activityViewModels()

咱们也从 Lifecycle 组件中受益良多。好比,咱们再也不使用自定义的生命周期方法 setUserVisibleHint,取而代之的是在添加 Fragment 到 ViewPager 或 Adapter 时调用统一的生命周期。这即是 ViewPager2 目前的工做机制,只有当前页面的 Fragment 会调用 onResume 方法。

// 设置只让当前展现的 Fragment 调用 onResume() 方法
class MyAdapter : FragmentPagerAdapter(BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT)

Fragment 的将来

前面讲过的功能大多在 Fragment 1.1 中已经提供,与此同时,咱们强烈建议使用 FragmentContainerView 容器来存储动态添加的 Fragment,而不要使用 FrameLayout 或其余布局。

固然,将来咱们还将对 Fragment 作出许许多多的改进,下面我就来介绍几个咱们当前正在进行的长期规划。不过要注意的是,接下来部份内容目前尚未正式推出,因此一些细节可能会有改变。

多重回退栈 (Multiple Back Stack)

首先要讲的是多重回退栈 (Multiple Back Stack)。咱们知道在 Android 中,老是会有一个 Activity 栈,而 Fragment 也实现了一样的结构,用于保存回退栈信息。而咱们想要实现的则是一种同时支持单一回退栈和多重回退栈的模型,好让屏幕上不可见的 Fragment 也能保存本身的状态,从而避免状态的丢失。与此相关的使用场景,比较典型的就是底部导航一类的导航视图。

下面是一个咱们的示例应用。咱们想要作的事情就是让应用中每一个底部标签页都拥有本身的栈,这样它们就能保存各自的状态。而当您在这些标签页间切换时,咱们也将帮您处理好从一个栈到另外一个栈时状态的保存和恢复。

Fragment 间的通信问题

咱们想要解决的另外一个问题与返回结果有关。

一直以来,诸如如何在 Fragment 间通信,或者说如何在 Android 的各类组件间通信的这类问题都深深困扰着咱们。想要在 Fragment 间通信,方法有不少,它们有好有坏。而这正体现出 Fragment 在这方面的 API 设计不佳。咱们能够设计一些用于 Fragment 间通信的 API,而且让它们在基于 Fragment 间互相持有依赖的前提下工做。可是这样的话,当前的 Fragment 将没法感知其它 Fragment 的生命周期。若是通信的 Fragment 处在不活跃的生命周期中,那么通信也将失败。

还有一个选项,是使用相似 onActivityResult 的 API。但咱们所考虑的,不仅是在 Fragment 之间通信,而是但愿能设计出一套公用的 API。它应当同时兼容 Activity、Fragment 等可能的导航组件,这样就算不知道对方的类型,也能创建通信。

简化 Fragment 的生命周期

最后要说的问题,是 Fragment 的生命周期。当前 Fragment 的生命周期十分复杂,它包含了两套不一样的生命周期。Fragment 本身的生命周期从它被添加到 FragmentManager 的时候开始,一直持续到它被 FragmentManager 移除并销毁为止;而 Fragment 所包含的视图,则有一个彻底分离的生命周期。当您的 Fragment 进入回退栈时,视图将会被销毁。但 Fragment 则会继续存活。

因而咱们产生了一个大胆的想法: 将二者合二为一会怎么样?在 Fragment 视图销毁时便销毁 Fragment,想要重建视图时就直接重建 Fragment,这样的话将大大减小 Fragment 的复杂度。而诸如 FragmentFactory 和状态保存一类,以往在 onConfigrationChange、 进程的死亡和恢复时使用的方法,在这种状况下将会成为默认选项。

固然,这个改动将会是十分的巨大。咱们目前处理的方式,是将它做为一个可选 API 加入到了 FragmentActivity 中。使用了这个新的 API,就能够开启生命周期简化过的新世界。

总结

咱们前面讲了 Fragment 一些历史问题的由来,以及咱们刚刚为它加入的一些特性,包括:

  • FragmentScenario,Fragment 的测试框架
  • FragmentFactory,统一的 Fragment 实例化组件
  • FragmentContainerView,Fragment 专属视图容器
  • OnBackPressedDispatcher,帮助您在 Fragment 或其余组件中处理返回按钮事件

最后还介绍了几个咱们仍在开发中的功能:

  • 多重回退栈
  • 使 Fragment 以及其余导航组件间能够优雅的通信
  • 简化 Fragment 的生命周期

但愿这些内容能够帮助您更好地使用和理解 Fragment。

咱们正努力将文中提到的新特性带给各位开发者,而在此以前,若是您在使用 Fragment 时有任何问题和疑惑,可使用 issuetracker.google.com 向咱们提交反馈或功能请求,谢谢!

您也能够经过视频回顾 2019 Android 开发者峰会演讲 —— Fragment 的过去、如今和未来

相关文章
相关标签/搜索