前言
很高兴见到你 ????,我是 Flywith24 。
最近 Android 官方针对 Fragment 文档进行了重新编写,使其适应 2020 年最佳实践的快速发展。
Fragment 的确是一个让开发者头疼的组件,它是一个很好的设计,但一直处于可改进的状态,随着 AndroidX Fragment 的快速更新,Fragment 已不同往日,虽然仍有改进的空间(单个 FragmentManager 不支持多返回栈,Fragment 自身和其 view 的生命周期不一致)。考虑到该文档的确有很多新知识以及官方文档的极慢的汉化速度,本文将 2020 版 Fragment 的官方文档翻译成中文
本文为下半部分,将介绍以下内容:
Fragment 的状态保存
Fragment 间通信
Fragment 于 AppBar 共同使用
使用 DialogFragment 显示 Dialog
Fragment 测试
上半部分介绍:
Fragment 的创建
Fragment manager
Fragment 事务
Fragment 动画
Fragment 生命周期
状态保存
各种 Android 系统操作可能会影响 fragment 的状态。为了确保用户状态得到保存,Android 会自动保存并还原 fragment 的返回栈。因此,您需要确保 fragment 中的所有数据也被保存和还原。
下表罗列了导致 fragment 丢失状态的操作,以及各种状态是否被保存。表中提到的状态类型如下:
Variables:fragment 的本地变量
View State:fragment 中 一个或多个 view 拥有 的所有数据
SavedState:该 fragment 实例固有的数据,应保存在
onSaveInstanceState()
中NonConfig:从外部源(例如服务器或本地存储库)提取的数据,或由用户创建一旦提交就发送到服务器的数据。
通常,Variables 与 SavedState 的处理方式相同,但下表将两者进行了区分,以展示各种操作对它们的影响:
*NonConfig state 在进程死亡时可以使用 Saved State module for ViewModel 保存状态。
让我们看一个具体的例子。我们生成一个随机字符串将其显示在 TextView
中,并提供一个发送给朋友之前编辑该字符串的选项:
用户按下编辑按钮后,将显示一个 EditText 视图,用户可以在其中编辑消息。如果用户点击CANCEL,则应清除 EditText 视图,并将其可见性设置为 View.GONE
。为了保持良好的体验,该示例需要管理 4 个数据:
以下各节介绍如何正确管理数据状态。
View State
View 负责管理自己的状态。例如,当 view 接受用户输入时,view 负责保存该输入以确保配置变化时能够恢复状态。所有 Android 官方提供的 view 均重写了 onSaveInstanceState()
和 onRestoreInstanceState()
方法,因此您不必管理 fragment 中的 View State。
???? 注意:为了确保配置更改是能够正确处理状态,您的自定义 View 应该重写
onSaveInstanceState()
和onRestoreInstanceState()
方法
例如,在前面的场景中,已编辑的字符串保存在 EditText 中。EditText 知道其显示的文本的值以及其它详细信息(如选定文本的开头和结尾)。
View 需要一个 ID 来恢复状态。这个 ID 必须在其所在的 fragment 视图树中唯一。没有 ID 的 View 不能恢复状态。
如表 1 所示,除了 fragment 被移除且没加入到返回栈和宿主销毁这两种情况,view 可以保存和恢复其 ViewState
。
SavedState
您的 fragment 负责管理少量动态状态,这些动态状态对于 fragment 的功能至关重要。您可以使用 Fragment.onSaveInstanceState(Bundle)
保存便于序列化的数据。与 Activity.onSaveInstanceState(Bundle)
相似,Bundle 中的数据将在 配置发生变化和系统资源回收 时保存,并且该 Bundle 在 fragment 的 onCreate(Bundle)
,onCreateView(LayoutInflater, ViewGroup, Bundle)
和 onViewCreated(View, Bundle)
方法中可用。
⚠️ 注意:fragment 的
onSaveInstanceState(Bundle)
仅在其宿主 activity 的onSaveInstanceState(Bundle)
调用时调用。
Tips:当使用 ViewModel
时,可用直接使用 SavedStateHandle
保存数据。更多的信息请参考:Saved State module for ViewModel(译者注:也可参考译者文章 绝不丢失的状态 androidx SaveState ViewModel-SaveState 分析)。
继续前面的示例,randomGoodDeed
是显示给用户的数据,isEditing
是确定 fragment 显示或隐藏 EditText
的标志。这种 save state 应使用 onSaveInstanceState(Bundle)
保存,如下所示:
// ???? Kotlin
override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
outState.putBoolean(IS_EDITING_KEY, isEditing)
outState.putString(RANDOM_GOOD_DEED_KEY, randomGoodDeed)
}
// ???? Java
@Override
public void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean(IS_EDITING_KEY, isEditing);
outState.putString(RANDOM_GOOD_DEED_KEY, randomGoodDeed);
}
要在 onCreate(Bundle)
中恢复状态,可用从 Bundle 中取值:
如表 1 所示,请注意,当 fragment 被加入到返回栈时 Variables 会被保存,将 Variables 看作 成 SavedState 来处理可以确保在所有场景下都能保存这些变量。
NonConfig
NonConfig 数据应放在 fragment 之外,例如在 ViewModel
中。在上面的示例中,seed
(NonConfig sate)在 ViewModel
中生成,由 ViewModel
负责保存其状态。
ViewModel
类本质上允许数据在配置发生变化(例如屏幕旋转)中幸存下来,并且在将 fragment 放回返回栈中时仍保留在内存中。在系统资源回收(进程死亡并重新创建)之后,将重新创建 ViewModel
,并生成一个新种子。在 ViewModel
中添加 SavedState
模块可以使 ViewModel
在系统资源回收的场景下保留其内部数据。
通信
为了复用 fragment,需要将每个 fragment 构建为完全独立的组件并定义自己的布局和行为。定义可复用的 fragment 并将它们与 activity 关联便可为 app 建立复合型 UI。
为了正确响应用户事件或共享状态信息,开发者通常需要在 activity 和它的 fragment 之间或两个到多个 fragment 之间建立通信。为了保证 fragment 的独立性,您 不应 让 fragment 与其它 fragment 或其宿主直接通信。
Fragment 库提供了两个通信选项:共享 ViewModel
和 Fragment Result API
。如何选择应视场景而定:要与任何自定义 API 共享持久数据,应使用 ViewModel
。对于可以放入 Bundle 的一次性的结果类数据,应使用 Fragment Result API
。
下文介绍如何使用 ViewModel
和 Fragment Result API
在 fragment 和 activity 之间通信。
使用 ViewModel 共享数据
ViewModel 是多个 fragment 或 fragment 与其宿主之间共享数据的理想选择。ViewModel 对象存储并管理 UI 数据。关于 ViewModel 的更多信息,请参考 ViewModel overview(译者注:也可参考译者文章 即使您不使用 MVVM 也要了解 ViewModel)。
与宿主 activity 共享数据
在某些场景下,您可能需要在 fragment 及其宿主 activity 之间共享数据。例如,您可能在 fragment 中操作全局的 UI 组件。
看看下面的 ItemViewViewModel
:
在上面的示例中,要存储的数据包装在 MutableLiveData
类中。LiveData
是可感知生命周期的可观察的数据持有者类。MutableLiveData
有着公开的更改值的方法。有关 LiveData 的更多信息,请参见 LiveData overview(译者注:也可参考译者文章 ViewModel 的左膀右臂 数据驱动真的香)。
通过将 activity 传递给 ViewModelProvider
构造器,您的 fragment 及其宿主 activity 都可以获取 activity 范围内共享的 ViewModel
实例,ViewModelProvider
负责实例化 ViewModel
或获取它(如果已经存在)。activity 和 fragment 都可以观察和修改该数据:
⚠️ 警告:请确保在
ViewModelProvider
中 使用合适的作用域。在上面的示例中,MainActivity
是MainActivity
和ListFragment
的作用域,因此它们能够获得相同的ViewModel
对象。如果ListFragment
改用自身的作用域,则将获得与MainActivity
不同的ViewModel
对象。
在 fragment 之间共享数据
同一 activity 中的两个或多个 fragment 通常需要相互通信。例如,一个 fragment 显示列表,另一个 fragment 允许用户将各种过滤选项筛选列表内容。如果没有 fragment 之间的直接通信,那么实现这种功能可能并不容易,这意味着这两个 fragment 不再是独立的。此外,两个 fragment 都必须处理另一个 fragment 尚未创建或不可见的情况。
这些 fragment 可以 使用所在 activity 范围内共享的 ViewModel
来处理通信。通过以这种方式共享 ViewModel
,fragment 之间无需彼此了解,并且 activity 无需执行任何操作即可完成通信。
以下示例显示两个 fragment 如何使用共享的 ViewModel
进行通信:
请注意,两个 fragment 都将其宿主 activity 作为 ViewModelProvider
的作用域。因为 fragment 使用相同的作用域,所以它们会获得相同的 ViewModel
实例,这使它们可以相互通信。
⚠️ 警告:
ViewModel
会保留在内存中,直到其作用域所在的ViewModelStoreOwner
永久消失。在单 activity 体系结构中,如果ViewModel
的作用域为 activity,则它实质上是一个单例。首次实例化ViewModel
之后,使用 activity 作用域获取ViewModel
将始终返回相同的现有ViewModel
实例和现有数据,直到 activity 的生命周期永久结束。
在父 fragment 和子 fragment 间共享数据
使用子 fragment 时,您的父 fragment 及其子 fragment 可能需要彼此共享数据。要在这些 fragment 之间共享数据,请 使用父 fragment 作为 ViewModel 的作用域。
Navigation Graph 范围内共享 ViewModel
如果您正在使用 Navigation library,还可以将 ViewModel
的作用域限定为目的地的 NavBackStackEntry
的生命周期。例如,可以将 ViewModel
的作用域限定为 ListFragment
的 NavBackStackEntry
:
ViewModel
作用域限定在
NavBackStackEntry
的更多信息,请参考 Interact programmatically with the Navigation component(译者注:也可以参考译者文章 想去哪就去哪,Android 世界的指南针)。
使用 Fragment Result API 获得结果
在某些情况下,您可能希望在两个 fragment 之间或 fragment 与其宿主 activity 之间传递一次性值。例如,您可能有一个读取二维码的 fragment,将数据传递回前一个 fragment。从 Fragment 1.3.0-alpha04 开始,每个 FragmentManager
都实现 FragmentResultOwner
。这意味着 FragmentManager
可以充当 fragment 结果的中央存储。此更改允许组件通过设置 fragment 结果并监听那些结果进而彼此通信,而无需那些组件彼此直接引用(译者注:Fragment Result API
引入的原因以及源码分析可参考 1.3.0-alpha04 来袭,Fragment 间通信的新姿势)。
在 fragment 之间传递结果
要将数据从 fragment B 传递回 fragment A,请首先在 fragment A 上设置一个结果 listener,该 fragment 将接收结果。在 fragment A 的 FragmentManager
上调用 setFragmentResultListener()
,如下所示:
在 fragment B(产生结果的 fragment)中,必须使用相同的 requestKey
在相同的 FragmentManager
上设置结果。您可以使用 setFragmentResult()
API 来做到这一点:
然后,fragment A 接收到结果,并在 fragment STARTED 后执行 listener 回调。
您只能有一个 listener 和给定 key 的结果。如果为同一 key 多次调用 setFragmentResult()
,并且 listener 未启动,则系统会将所有待处理的结果替换为更新的结果。如果设置的结果没有相应的 listener 接收,则结果将存储在 FragmentManager
中,直到您使用相同的 key 设置 listener 为止。listener 收到结果并触发 onFragmentResult()
回调后,该结果将被清除。此行为有两个主要含义:
返回栈上的 fragment 只有弹出并处于
STARTED
才能接收结果当 fragment 正在监听一个
STARTED
状态的结果,当结果被设置则立即触发 listener 的回调
???? 注意:由于 fragment 结果存储在
FragmentManager
层级上,因此必须将 fragment attach 到父FragmentManager
来调用setFragmentResultListener()
或setFragmentResult()
。
测试 fragment 结果
使用 FragmentScenario 测试 setFragmentResult()
和 setFragmentResultListener()
的调用。使用 launchFragmentInContainer 或 launchFragment 为被测 fragment 创建一个场景,然后手动调用待测试的方法。
要测试 setFragmentResultListener()
,请创建带有 fragment 的场景,该 fragment 将调用 setFragmentResultListener()
。接下来,直接调用 setFragmentResult()
并验证结果:
在父 fragment 和子 fragment 间传递结果
要将结果从子 fragment 传递给父 fragment,在调用 setFragmentResultListener()
时,父 fragment 应使用 getChildFragmentManager()
而不是 getParentFragmentManager()
。
接收宿主 activity 的结果
要在宿主 activity 中接收 fragment 结果,请使用 getSupportFragmentManager()
在 FragmentManager
上设置结果 listener。
与 AppBar 共同使用
顶部 app bar 在 app 窗口顶部提供了统一的界面,用于显示当前屏幕上的信息和操作。
使用 fragment 时,app bar 可以作为宿主 activity 的 ActionBar 或 fragment 布局中的 toolbar。app bar 的所属权取决于您的应用需求。
如果所有屏幕都使用始终位于顶部并填满屏幕宽度的同一 app bar,则应使用由该 activity 托管的主题提供的 action bar。使用主题 app bar 有助于保持一致的外观,并提供了一个存放选项菜单和返回按钮的地方。
如果要在多个屏幕上对 ap bar 的大小,位置和动画进行更多控制,请使用由 fragment 托管的 toolbar。例如,您可能需要折叠的 app bar 或宽度为屏幕一半且垂直居中的 app bar。
了解不同的方式并采用正确的方法可以节省您的时间,并助于确保您的 app 正常运行。对于加载菜单和响应用户交互的操作要根据不同场景使用不同的方法处理。
下面的示例包含可编辑配置文件的 ExampleFragment
。该 fragment 在其 app bar 中加载了以下 XML-defined menu:
该菜单包含两个选项:一个用于导航到配置文件界面,另一个用于保存对配置文件所做的所有更改。
Activity 拥有的 app bar
app bar 通常由宿主 activity 持有。当 activity 持有 app bar 时,fragment 可以通过重写在 fragment 创建期间调用的 framework 方法来与 app bar 进行交互。
???? 注意:本节内容仅在 activity 持有 app bar 时才适用。如果您的 app bar 是 fragment 布局中包含的 toolbar,请参见 Fragment 拥有的 app bar 一节。
注册 activity
您必须通知系统您的 app bar fragment 正在参与选项菜单的加载。为此,请在 fragment 的 onCreate(Bundle)
方法中调用 setHasOptionsMenu(true)
,如下所示:
setHasOptionsMenu(true)
告诉系统您的 fragment 想接收菜单相关的回调。当发生与菜单相关的事件(创建,点击等)时,首先在 activity 上调用事件处理方法,然后再在 fragment 上调用该事件处理方法。请注意,您的应用程序逻辑不应依赖于此顺序。如果同一 activity 托管多个 fragment,则每个 fragment 都可以提供菜单选项。在这种情况下,回调顺序取决于 fragment 的添加顺序。
加载 menu
要将菜单合并到 app bar 的选项菜单中,请在 fragment 中重写 onCreateOptionsMenu()
。此方法接收当前 app bar 菜单和 MenuInflater
作为参数。使用 menu inflater 创建 fragment 菜单的实例,然后将其合并到当前菜单中,如下所示:
处理点击事件
参与选项菜单的每个 activity 和 fragment都能够响应触摸事件。Fragment的 onOptionsItemSelected()
接收选定的菜单 item 作为参数,并返回一个布尔值以指示是否已消费了触摸。一旦 activity 或 fragment 从 onOptionsItemSelected()
返回 true,其它任何参与的 fragment 将不会收到回调。
在 onOptionsItemSelected()
的实现中,在菜单 item 的 itemId 上使用 switch
语句(Kotlin 使用 when
关键字)。如果所选 item 属于您,则处理触摸并返回 true 表示已处理 click 事件。如果所选项目不是您的,请调用 super 方法。默认情况下,super 方法返回 false 以允许菜单被后续处理。
???? 注意:fragment 只能处理通过
onCreateOptionsMenu()
调用添加的菜单 item 。使用 activity 拥有的 app bar 时,activity 应处理返回上一级按钮和未被 fragment 添加的菜单 item 的点击事件。
动态修改菜单
隐藏/显示按钮或更改图标的逻辑应放在 onPrepareOptionsMenu()
中。在显示菜单的每个实例之前立即调用此方法。
继续前面的示例,在用户开始编辑之前,保存按钮应该是不可见的,并且在用户保存后应该消失。将此逻辑添加到 onPrepareOptionsMenu()
可以确保始终正确显示菜单:
当您需要更新菜单时(例如,当用户按下编辑按钮以编辑配置文件信息时),您必须在宿主 activity 上调用 invalidateOptionsMenu()
以请求系统调用 onCreateOptionsMenu()
。无效时,您可以在 onCreateOptionsMenu()
中进行更新。菜单加载后,系统将调用 onPrepareOptionsMenu()
并更新菜单以响应 fragment 的当前状态。
Fragment 拥有的 app bar
如果您的 app 中的大多数屏幕都不需要应用 app bar,或者一个屏幕可能需要一个截然不同的 app bar,则可以在 fragment 布局中添加 Toolbar。尽管您可以在 fragment 的视图树中的任何位置添加 Toolbar
,但通常应将其放置在屏幕顶部。要在片段中使用 Toolbar
,请提供一个 ID 并在 fragment 中获得对其的引用,就像在其他任何视图中一样。
使用 fragment 拥有的 app bar 时,强烈建议直接使用 Toolbar API。不要使用 setSupportActionBar()
和 Fragment menu API,它们仅适用于 activity 拥有的 app bar。
加载 menu
Toolbar
有一个便捷方法 inflateMenu(int)
,它需要菜单资源的 ID 作为参数。要将 XML 菜单资源加载到 toolbar 中,请将 resId
传递给此方法,如下所示:
要加载另一个 XML 菜单资源,请使用新菜单的 resId
再次调用该方法。新菜单 item 将添加到菜单,并且现有菜单 item 不会被修改或删除。
如果要替换现有菜单集,请在使用新菜单 ID 调用 inflateMenu(int)
之前清除菜单。
// ???? Kotlin
class ExampleFragment : Fragment() {
...
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
viewBinding.myToolbar.setOnMenuItemClickListener {
when (it.itemId) {
R.id.action_settings -> {
// 跳转设置界面
true
}
R.id.action_done -> {
// 保存配置更改
true
}
else -> false
}
}
}
}
// ???? Java
public class ExampleFragment extends Fragment {
...
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
...
viewBinding.myToolbar.setOnMenuItemClickListener(item -> {
switch (item.getItemId()) {
case R.id.action_settings:
// 跳转设置界面
return true;
case R.id.action_done:
// 保存配置更改
return true;
default:
return false;
}
});
}
}
处理点击事件
您可以使用 setOnMenuItemClickListener()
方法将 OnMenuItemClickListener
直接传递到 toolbar。每当用户从 toolbar 操作菜单 item 时,就会调用此 listener。选定的 MenuItem
传递给 listener 的 onMenuItemClick()
方法,并消费这个事件,如下所示:
动态修改菜单
当 fragment 拥有 app bar 时,您可以在运行时像操作其他 view 一样修改 Toolbar
。
继续前面的示例,在用户开始编辑之前,保存菜单 item 应该是不可见的,并且在点击保存后应再次消失:
添加导航图标
如果存在,导航按钮将出现在工具栏的起始位置,在 toolbar 上设置导航图标并使其可见。您还可以设置 navigation 特有的 onClickListener()
,只要用户点击导航按钮,就会调用该 onClickListener
,如下所示:
???? 注意:使用
Toolbar
API 处理导航图标时,不会触发默认 activity 的行为。您可以使用requireActivity().onSupportNavigateUp()
触发返回到 manifest 中定义的 父 activity 的行为。
使用 DialogFragment 显示 Dialog
DialogFragment
是专门用于创建和托管 dialog 的特殊 fragment 子类。严格来说,您不需要在 fragment 中托管 dialog,但是这样做可以使 FragmentManager
管理 dialog 的状态并在配置发生变化时自动还原 dialog 。
???? 注意:本节假定您熟悉创建 dialog 。有关更多信息,请参见 dialog 指南。
创建 DialogFragment
要创建 DialogFragment
,请首先创建一个继承 DialogFragment
的类,并重写 onCreateDialog()
,如下所示:
与 onCreateView()
在普通 fragment 中创建根视图的方式类似,onCreateDialog()
应该创建一个 Dialog
来显示为 DialogFragment
的一部分。DialogFragment
可以在 fragment 的生命周期中的适当状态下显示 Dialog
。
???? 注意:
DialogFragment
拥有Dialog.setOnCancelListener()
和Dialog.setOnDismissListener()
回调。您不能自己设置它们。要了解有关这些事件的信息,请重写onCancel()
和onDismiss()
。
就像 onCreateView()
一样,您可以从 onCreateDialog()
返回 Dialog
的任何子类,而不仅限于使用 AlertDialog
。
显示 DialogFragment
无需手动创建 FragmentTransaction
即可显示 DialogFragment
,使用 show()
方法显示 dialog 。您可以向该方法传递一个 FragmentManager
对象和 FragmentTransaction
的 tag(String 类型)。从 Fragment 中创建 DialogFragment
时,必须使用 Fragment 的子 FragmentManager
来确保在配置发生变化后正确恢复状态。非空标记允许您在以后使用 findFragmentByTag()
来获取 DialogFragment
。
为了更好地控制 FragmentTransaction
,可以使用 show()
的重载方法传入一个 FragmentTransaction
。
???? 注意:因为
DialogFragment
是在配置发生变化后自动恢复的,所以请考虑仅根据用户操作或findFragmentByTag()
返回 null(代表 dialog 不存在)时才调用show()
。
DialogFragment 生命周期
DialogFragment
遵循标准的 fragment 生命周期。此外,DialogFragment
还有一些其它的生命周期回调。常见的如下:
onCreateDialog()
- 重写此回调,为 fragment 提供一个管理和显示的 dialogonDismiss()
- 如果在关闭 Dialog 时需要执行自定义逻辑(例如释放资源,取消订阅可观察的资源等),请重写此回调onCancel()
- 如果在取消 Dialog 时需要执行自定义逻辑,则重写该方法
DialogFragment
还包含用于关闭或设置 DialogFragment
可取消的方法:
dismiss()
- 关闭 fragment 及其 dialog 。如果该 fragment 加入到了返回栈,则弹出该 fragment 及其顶部的所有 entry。否则,将提交一个新的事务 remove 该 fragment。setCancellable()
- 控制当前显示的 dialog 是否可以取消。应该使用DialogFragment
的该方法而不是直接调用Dialog.setCancelable(boolean)
。
请注意,在将 DialogFragment
与 Dialog
一起使用时,您不要重写 onCreateView()
或 onViewCreated()
。dialog 不仅是 view,它还具有自己的 Windiow。因此,重写 onCreateView()
是不行的。此外,除非您已重写 onCreateView()
并提供了非 null 的 view,否则永远不会在自定义 DialogFragment
上调用 onViewCreated()
。
???? 注意:订阅支持生命周期的组件(如 LiveData)时,切勿在使用
Dialog
的DialogFragment
中将viewLifecycleOwner
用作LifecycleOwner
。相反,请使用DialogFragment
本身,或者如果您使用的是 Jetpack Navigation,请使用NavBackStackEntry
。
使用自定义 View
您可以通过 重写 onCreateView()
来创建 DialogFragment
并显示 dialog ,可以像使用正常的 fragment 一样为其提供 layoutId
,也可以使用 Fragment 1.3.0-alpha02 中引入的 DialogFragment
构造器。
由 onCreateView()
返回的 View 将自动添加到 dialog 中。在大多数情况下,这意味着您不需要重写 onCreateDialog()
,因为默认的空 dialog 是用 传入的 view 填充的。
某些 DialogFragment
的子类,例如 BottomSheetDialogFragment
,会将您的 view 嵌入到一个样式为底部弹窗的 dialog 中。
测试
本节内容介绍如何使用框架提供的 API 测试 fragment 的行为。
Fragment 作为 app 中的可复用的容器,使您可以在各种 activity 和布局配置中呈现相同的 UI 界面。考虑到 fragment 的通用性,重要的是要验证它们是否提供了一致且资源高效的体验。请注意以下几点:
你的 fragment 不应依赖特定的父 activity 或 fragment
除非 fragment 对用户可见,否则不应创建 fragment 视图树
为了提供这些测试条件,AndroidX fragment-testing 库提供了 FragmentScenario
创建 fragment 并 改变它们的 Lifecycle.State
。
???? 注意:要成功运行包含
FragmentScenario
对象的测试,请在测试的 instrumentation 线程中运行 API 的方法。要了解有关 Android 测试中使用的不同线程的更多信息,请参阅 Understand threads in tests。
声明依赖
要使用 FragmentScenario
,请使用 debugImplementation
在 app 的 build.gradle
文件中定义 fragment 测试工件,如下所示:
本节示例使用的断言来自 Espresso 和 Truth。
创建 Fragment
FragmentScenario
包括以下用于在测试中启动 fragment 的方法:
launchInContainer()
,用于测试 fragment 的 UI。FragmentScenario
将 fragment attach 到 activity root view 容器内,该 activity 除此之外啥都没有。launch()
,用于测试没有 UI 的 fragment。FragmentScenario
将 这种类型的 fragment attach 到一个没有 root view 的空 activity 中。
启动其中一种 fragment 时,FragmentScenario
将被 fragment 驱动为 RESUMED
状态。此状态代表该 fragment 正在运行并且对用户可见。您可以使用 Espresso UI tests 测试相关 UI 元素的信息。
以下代码示例演示如何使用每种方法启动 fragment:
???? 注意:您的 fragment 可能要求测试 activity 不使用默认的主题。您可以提供自己的主题作为
launch()
和launchInContainer()
的参数。
launchInContainer() 示例
launch() 示例
提供依赖
如果您的 fragment 具有依赖项,则可以通过向 launchInContainer()
或 launch()
方法提供自定义 FragmentFactory
来提供这些依赖项的测试版本:
有关使用 FragmentFactory
为 Fragment 提供依赖项,请参考 FragmentManager
一节。
将 fragment 驱动到新状态
在 app 的 UI 测试中,通常需要 fragment 处于 RESUMED
状态时开始测试。但是,在更细粒度的单元测试中,当 fragment 从一种生命周期状态转换为另一种生命周期状态时,您可能也需要测试其行为。
要将 fragment 驱动到不同的生命周期状态,请调用 moveToState()
。此方法支持以下状态作为参数:CREATED
,STARTED
,RESUMED
和 DESTROYED
。该方法模拟了该 fragment 或其宿主 activity 由于一些原因驱动 fragment 状态更改的场景。
???? 注意:如果将 fragment 切换为
DESTROYED
状态,则无法将该 fragment 驱动为另一状态,也不能将该 fragment attach 到其它 activity。
以下示例将测试 fragment 移至 CREATED
状态:
⚠️ 警告:如果您尝试将被 fragment 段转换为当前状态,则
FragmentScenario
将忽略该请求而不会引发异常。特别是,API 允许您连续多次将 fragment 转换为 DESTROYED 状态。
重新创建 Fragment
如果您的 app 在资源不足的设备上运行,则系统可能会销毁包含您的 fragment 的 activity。这种情况要求您的 app 在用户返回 fragment 时重新创建该 fragment。为了模拟这种情况,请调用 recreate()
:
FragmentScenario.recreate()
销毁 fragment 及其宿主activity,然后重新创建它们。当 FragmentScenario
类重新创建被测试的 fragment 时,该 fragment 将返回其在销毁之前所处的生命周期状态。
与 fragment UI 交互
要在被测 fragment 中触发 UI 操作,请使用 Espresso view matchers 与视图中的元素进行交互:
如果需要调用 fragment 自身的方法,例如响应选项菜单中的选择,则可以使用 FragmentScenario.onFragment()
获取对 fragment 的引用并传递 FragmentAction
来安全地进行操作:
???? 注意:不要保留对传递给
onFragment()
的 fragment 的引用。这些引用消耗系统资源,并且引用本身可能已过时,因为框架可以重新创建 fragment。
测试 dialog fragment
FragmentScenario
还支持测试 dialog fragment。尽管 dialog fragment 具有 UI 元素,但它们的布局填充在单独的 Window 中,而不是 activity 本身。因此,请使用 FragmentScenario.launch()
测试 dialog fragment。
下面的示例测试 dialog 的关闭过程:
新版文档的变化
在 xml 使用
FragmentContainerView
作为 fragment 的容器,不要使用<fragment>
标签或 FrameLayout建议使用 Navigation library 管理 app 内导航
activity 中使用
getSupportFragmentManager()
获取FragmentManager
fragment 中使用
getChildFragmentManager()
获取管理子 fragment 的FragmentManager
fragment 使用
getParentFragmentManager()
获取其宿主的FragmentManager
commit 事务时建议调用
setReorderingAllowed(true)
Fragment 有一个带有
layoutId
参数的构造器,无需调用onCreateView
设置布局FragmentFactory 默认使用无参构造器创建 fragment,如果自定义了 fragment 构造器,为了保证重建时的一致性,需要自定义 FragmentFactory
使用
setMaxLifecycle()
限制了 Fragment 的最大生命周期,因此 setUserVisibleHint 被弃用了,保证了 ViewPager 中 fragment 可见性判断与正常情况一致
对于涉及多种动画效果的场景时建议使用 transition,嵌套
AnimationSet
存在已知问题。理解 Fragment Lifecycle,其 View Lifecycle 以及相应回调方法的关系
理解 observe LiveData 时
lifecycleOwner
和viewLifecycleOwner
的区别使用 ViewModel 配合 savestate 保存/恢复状态
使用最新的 Fragment 通信机制:共享
ViewModel
和Fragment Result API
使用
DialogFragment
来显示 Dialog,能够更好的处理配置发生变化和系统资源回收的场景fragment-ktx
库有很多方便的扩展函数和属性代理fragment library
中包含了activity library
作者:Flywith24
链接:https://juejin.cn/post/6901453354463920135
关注我获取更多知识或者投稿