Fragment 是 Android 中历史非常悠久的一个组件,它在 API 11 被退出,时至今日已成为 Android 开发中最罕用的组件之一。Fragment 有了哪些新个性、修复了哪些问题,都是开发者们十分关心的话题。上面咱们就来从新说一说 Fragment —— 不仅仅是说当初的 Fragment,还会回顾它的倒退,并让您一瞥它将来的样子。
Fragment 的诞生与倒退
不晓得您是否还记得 “ 上古期间 ”,在那些还没有 Fragment 的日子,简直所有逻辑都被放在了 Activity 中,使得 Activity 臃肿而又凌乱。此时,Fragment 作为一个微型 Activity 而诞生,迈出了缩减 Acitvity 之路上的一小步。
不过 “ 欲戴王冠,必承其重 ”,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 正是由此而来。
Fragment 的现状
咱们的想法产生扭转,还是 2011 年的事,到明天曾经经验了很长的工夫。这期间咱们破费了很多的精力去从新构思 Fragment 的定位。咱们心愿 Fragment 成为一个真正的外围组件,它应该领有可预测的、正当的行为,不应该呈现随机谬误,也不应该毁坏现有的性能。
其实咱们心愿挑个工夫公布 Fragment 的 2.0 版,它将只蕴含那些新的、好用的 API。但在时机成熟之前,咱们会在现有的 Fragment 中逐渐退出新的并弃用旧的 API,并为旧性能提供更好的代替计划。当没人再应用已弃用的 API 时,迁徙到 Fragment 2.0 就会变得很容易。接下来我就来讲讲,咱们为此所做的一些工作。
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 的过来、当初和未来。