2020 年 Fragment 最新文档(下),该更新知识库啦

前言

很高兴见到你 ????,我是 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:从外部源(例如服务器或本地存储库)提取的数据,或由用户创建一旦提交就发送到服务器的数据。

通常,VariablesSavedState 的处理方式相同,但下表将两者进行了区分,以展示各种操作对它们的影响:

*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 库提供了两个通信选项:共享 ViewModelFragment Result API。如何选择应视场景而定:要与任何自定义 API 共享持久数据,应使用 ViewModel。对于可以放入 Bundle 的一次性的结果类数据,应使用 Fragment Result API

下文介绍如何使用 ViewModelFragment 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使用合适的作用域。在上面的示例中,MainActivityMainActivityListFragment 的作用域,因此它们能够获得相同的 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 的作用域限定为 ListFragmentNavBackStackEntry

关将 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 提供一个管理和显示的 dialog

  • onDismiss() - 如果在关闭 Dialog 时需要执行自定义逻辑(例如释放资源,取消订阅可观察的资源等),请重写此回调

  • onCancel() - 如果在取消 Dialog 时需要执行自定义逻辑,则重写该方法

DialogFragment 还包含用于关闭或设置 DialogFragment 可取消的方法:

  • dismiss() - 关闭 fragment 及其 dialog 。如果该 fragment 加入到了返回栈,则弹出该 fragment 及其顶部的所有 entry。否则,将提交一个新的事务 remove 该 fragment。

  • setCancellable() - 控制当前显示的 dialog 是否可以取消。应该使用 DialogFragment 的该方法而不是直接调用 Dialog.setCancelable(boolean)

请注意,在将 DialogFragmentDialog 一起使用时,您不要重写  onCreateView()onViewCreated()。dialog 不仅是 view,它还具有自己的 Windiow。因此,重写 onCreateView()是不行的。此外,除非您已重写 onCreateView() 并提供了非 null 的 view,否则永远不会在自定义 DialogFragment 上调用 onViewCreated()

???? 注意:订阅支持生命周期的组件(如 LiveData)时,切勿在使用 DialogDialogFragment 中将 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()。此方法支持以下状态作为参数:CREATEDSTARTEDRESUMEDDESTROYED。该方法模拟了该 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 时 lifecycleOwnerviewLifecycleOwner 的区别

  • 使用 ViewModel 配合 savestate 保存/恢复状态

  • 使用最新的 Fragment 通信机制:共享 ViewModelFragment Result API

  • 使用 DialogFragment 来显示 Dialog,能够更好的处理配置发生变化和系统资源回收的场景

  • fragment-ktx 库有很多方便的扩展函数和属性代理

  • fragment library 中包含了 activity library


作者:Flywith24
链接:https://juejin.cn/post/6901453354463920135

关注我获取更多知识或者投稿

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值