【译】2020 年 Fragment 最新文档(上),该更新知识库啦

6,267 阅读38分钟

前言

很高兴见到你 👋,我是 Flywith24 。

最近 Android 官方针对 Fragment 文档进行了重新编写,使其适应 2020 年最佳实践的快速发展。

Fragment 的确是一个让开发者头疼的组件,它是一个很好的设计,但一直处于可改进的状态,随着 AndroidX Fragment 的快速更新,Fragment 已不同往日,虽然仍有改进的空间(单个 FragmentManager 不支持多返回栈,Fragment 自身和其 view 的生命周期不一致)。考虑到该文档的确有很多新知识以及官方文档极慢的汉化速度,本文将 2020 版 Fragment 的官方文档翻译成中文,喜欢一手信息的小伙伴可直奔 官方原文。如果只想关注新文档中的变化,可 点此直达。限于篇幅原因,该文档分上下两部分。

【译】2020 年 Fragment 最新文档(上),该更新知识库啦

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

本文将介绍以下内容:

  • Fragment 的创建
  • Fragment manager
  • Fragment 事务
  • Fragment 动画
  • Fragment 生命周期

第二部分将介绍:

  • Fragment 的状态保存
  • Fragment 间通信
  • Fragment 与 AppBar 共同使用
  • 使用 DialogFragment 显示 Dialog
  • Fragment 测试

点击查看彩蛋 😉 😝 欢迎来到彩蛋部分,您一定是个好奇心很强的小伙伴呢。

我是一个「强迫症晚期患者」,为了移动端更好阅读的体验,我经常将代码以图片的形式插入到文内。但随之而来出现一个问题:没办法 copy 代码(这对 cv 开发者很重要的 🤣)。

前些天,我在 github 某个项目的 README 文档中看到一个技巧,便是把较长且有些影响阅读的内容折叠,读者可以自由地选择展开。

这也是这个「彩蛋」的显示方式。后文中关于代码的部分我都会提供图片和可复制的源码两部分,其中后者处于折叠状态。您可以点击 「点击查看代码详情」以展开源码。

彩蛋结束。🥳

总览

一个 Fragment 代表了 开发者 app UI 可重用的部分。Fragment 定义和管理了自己的布局,拥有自己的生命周期,并且可以处理自己的输入事件。Fragment 不能独自存在——它们必须有一个 activity 或 fragment 作 宿主。Fragment 的视图树是其宿主视图树的一部分,或者附加到其宿主的视图树上。

🌟 注意:某些 Android Jetpack 库,如 NavigationBottomNavigationViewViewPager2 是被设计为与 Fragment 配合使用的。

模块化

Fragment 允许您将 UI 分成分散的块,从而将 模块化可重用性 引入到您的 activity UI 中。Activity 是放置 app UI 全局元素(如 navigation drawer)的理想场所。相对的,Fragment 更适合于定义和管理整个屏幕或部分屏幕的 UI。

思考设计一个适用各种屏幕尺寸的 app。在较大尺寸的屏幕上,该 app 应该以静态 navigation drawer 和网格 list 的形式展示。在较小尺寸的屏幕上,该 app 应该显示 bootom navigation bar 和线性 list 的形式。管理 activity 中的这些变化可能很麻烦。将导航元素与内容分开可以使此过程更易于管理。之后 activity 负责显示正确的 navigation UI,fragment 负责显示正确布局的列表。

图1

上图展示了同一个界面的两个版本。左侧的大尺寸屏幕包含一个由 activity 控制的 navigation drawer 和 一个由 fragment 控制的网格列表。右侧小尺寸屏幕包含一个由 activity 控制的 bottom navigation bar 和一个由 fragment 控制的线性列表。

将 UI 分离成多个 fragment 有助于更轻松地在运行时修改 activity 的外观。当您的 activity 处于 STARTED lifecycle state 或更高的状态时,可以添加/替换/移除 fragment。你可以将这些更改记录到一个由 activity 管理的 返回栈 中,以允许恢复之前的状态。

您可以在一个 activity 、多个 activity 甚至是 fragment 中(嵌套场景)使用相同的 fragment 实例。考虑到这一点,您应该在 fragment 中仅提供管理自己 UI 所需的逻辑,避免在 fragment 内部依赖或操作另一个 fragment。

创建

本节介绍如何创建一个 fragment 并将其添加到 activity 中。

配置环境

Fragment 要求依赖 AndroidX Fragment library。您需要在 project 的 build.gradle 文件中添加 Google Maven repository

点击查看代码详情
buildscript {
    ...

    repositories {
        google()
        ...
    }
}

allprojects {
    repositories {
        google()
        ...
    }
}

欲将 AndroidX Fragment library 添加到您的项目中,请在您的 app 的 build.gradle 文件中添加以下依赖:

点击查看代码详情
dependencies {
    def fragment_version = "1.2.5"

    // Java 语言使用
    implementation "androidx.fragment:fragment:$fragment_version"
    // Kotlin 语言使用
    implementation "androidx.fragment:fragment-ktx:$fragment_version"
}

创建 fragment class

创建 fragment,继承 AndroidX Fragment,重写其方法并插入 app 的逻辑与创建 Activity 的方式类似。想要创建定义其自己的布局的最小 fragment,请将您的 fragment 的布局资源提供给主构造器,如以下所示:

点击查看代码详情
// Kotlin
class ExampleFragment : Fragment(R.layout.example_fragment)

// Java
class ExampleFragment extends Fragment {
    public ExampleFragment() {
        super(R.layout.example_fragment);
    }
}

Fragment 库也提供了一些特殊作用的 fragment 类:

  • DialogFragment

    展示一个浮动 dialog。使用该类创建一个 dialog 是一种在 Activity 中使用 dialog 的很好替代方法,因为 fragment 会自动处理 dialog 的创建与清除。

  • PreferenceFragmentCompat

    Preference 对象的层次结构显示为列表。你可以使用 PreferenceFragmentCompat 来为您的 app 创建一个设置界面

在 activity 中添加 fragment

通常,您的 fragment 必须嵌入一个 AndroidX FragmentActivity 中才能作为 activity layout 的部分 UI。FragmentActivity 是 AppCompatActivity 的父类,因此如果您已经继承了 AppCompatActivity 则无需做任何更改。

将 fragment 添加到 activity 的视图树中有两种方式:

  • 在 activity layout 文件中定义 fragment
  • 在 activity layout 文件中定义 fragment container 并且稍后通过代码的方式在 activity 中添加 fragment。

无论哪种方式,您都需要添加一个 FragmentContainerView 来定义 fragment 在 activity 视图树中的位置。强烈建议始终使用 FragmentContainerView 作为 fragment 的容器,因为 FragmentContainerView 修复了一些 fragment 的 bug,其它 ViewGroup(如 FrameLayout)并没提供修复。(译者注:之前 fragment 在 Z 轴的顺序有些问题,FragmentContainerView 修复了该问题)

通过 XML 添加 fragment

请使用 FragmentContainerView 标签来在 XML 中声明 fragment。下面是包含单个 FragmentContainerView 的 activity 布局文件:

点击查看代码详情
<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:name="com.example.ExampleFragment" />

android:name 属性指定了待实例化的 Fragment 类名。当 activity 的 layout 被 inflated 时,将实例化指定的 fragment,在新的已实例化的 fragment 中调用 onInflate() ,并创建一个 FragmentTransaction 将 fragment 添加到 FragmentManager 上。

🌟 注意:您可以使用 class 来替换 android:name 用来指明待实例化的 fragment

通过代码添加 fragment

若要以代码的方式将 fragment 添加到 activity 布局中,该布局应该包含一个 FragmentContainerView 用于作为 fragment 的容器,如下面示例:

点击查看代码详情
<!-- res/layout/example_activity.xml -->
<androidx.fragment.app.FragmentContainerView
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:id="@+id/fragment_container_view"
    android:layout_width="match_parent"
    android:layout_height="match_parent" />

与使用 XML 的方式不同,此处的 FragmentContainerView 上并未使用 android:name 属性,因此不会自动实例化特定的 fragment 而是在代码中使用 FragmentTransaction 实例化一个 fragment 并将其添加到 activity 的布局中。

当 activity 运行时,您可以使用 fragment 事务 添加/移除/替换 一个 fragment。在 FragmentActivity 中,您可以获取一个 FragmentManager 实例,该实例可用于创建一个 FragmentTransaction。然后您可以在 activity 的 onCreate() 方法中使用 FragmentTransaction.add() 实例化 fragment,接着传入容器的 ID 和 fragment class,然后提交事务。如下面的示例:

点击查看代码详情
// Kotlin
class ExampleActivity : AppCompatActivity(R.layout.example_activity) {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 防止系统资源回收或配置发生变化 fragment 发生重叠的问题
        if (savedInstanceState == null) {
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add<ExampleFragment>(R.id.fragment_container_view)
            }
        }
    }
}

// Java
public class ExampleActivity extends AppCompatActivity {
    public ExampleActivity() {
        super(R.layout.example_activity);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
      	// 防止系统资源回收或配置发生变化 fragment 发生重叠的问题
        if (savedInstanceState == null) {
            getSupportFragmentManager().beginTransaction()
                .setReorderingAllowed(true)
                .add(R.id.fragment_container_view, ExampleFragment.class, null)
                .commit();
        }
    }
}

🌟 注意:执行 FragmentTransaction 时,应 始终 使用 setReorderingAllowed(true),更多关于事务重新排序的内容,请移步 Fragment 事务一节。

在上一个示例中,仅当 saveInstanceStatenull 时才创建 fragment 事务。这是为了确保当 activity 第一次 create 时 fragment 仅被添加一次。当系统资源回收或配置发生变化时 saveInstanceState 不再 为 null 且无需再次添加该 fragment,因为 fragment 会自动从 savedInstanceState 中恢复。

如果 fragment 需要初始化数据,则可以通过在 FragmentTransaction.add() 当调用中提供 Bundle 将参数传递给 fragment,如下所示:

点击查看代码详情
// Kotlin
class ExampleActivity : AppCompatActivity(R.layout.example_activity) {
      override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        if (savedInstanceState == null) {
            val bundle = bundleOf("some_int" to 0)
            supportFragmentManager.commit {
                setReorderingAllowed(true)
                add<ExampleFragment>(R.id.fragment_container_view, bundle)
            }
        }
    }
}

// Java
public class ExampleActivity extends AppCompatActivity {
    public ExampleActivity() {
        super(R.layout.example_activity);
    }
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        if (savedInstanceState == null) {
            Bundle bundle = new Bundle();
            bundle.putInt("some_int", 0);

            getSupportFragmentManager().beginTransaction()
                .setReorderingAllowed(true)
                .add(R.id.fragment_container_view, ExampleFragment.class, bundle)
                .commit();
        }
    }
}

然后,可以通过调用 requireArguments() 从 fragment 中获得 Bundle,并且可以使用相应的 Bundle getter 方法来取出每个参数:

点击查看代码详情
// Kotlin
class ExampleFragment : Fragment(R.layout.example_fragment) {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        val someInt = requireArguments().getInt("some_int")
        ...
    }
}

// Java
class ExampleFragment extends Fragment {
    public ExampleFragment() {
        super(R.layout.example_fragment);
    }

    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        int someInt = requireArguments().getInt("some_int");
        ...
    }
}

Fragment manager

🌟 注意:我们强烈推荐使用 Navigation library 来管理您 app 的导航。该框架遵循有关处理 fragment,返回栈以及 fragment manager 的最佳实践。想要获取更多关于 Navigation 的信息,参见: Get started with the Navigation componentMigrate to the Navigation component

FragmentManager 是负责对 app 的 fragment 执行操作的类,如添加/移除/替换 fragment 并将这些操作加入到返回栈中。

如果您使用的是 Jetpack Navigation library,则可能永远不会直接与 FragmentManager 进行交互,因为它将 FragmentManager 使用的部分封装了起来。换句话说,任何 app 使用 fragment 都在某种层次上使用 FragmentManager,因此了解它的含义和工作方式非常重要。

本节内容介绍如何访问 FragmentManager,与 activity 和 fragment 相关的 FragmentManager 的角色,使用 FragmentManager 管理返回栈以及为 fragment 提供数据和依赖。

访问 FragmentManager

在 activity 中访问

每个 FragmentActivity 及其子类(如 AppCompatActivity)都可以通过 getSupportFragmentManager() 来访问 FragmentManager

在 fragment 中访问

Fragment 也能管理一个或多个子 fragment(译者注:嵌套 fragment,即一个 fragment 的直接宿主可能是 activity 或另一个 fragment)。在 fragment 中,您可以通过 getChildFragmentManager() 来获取管理子 fragment 的 FragmentManager 实例。如果需要访问该 fragment 宿主的 FragmentManager,可以使用 getParentFragmentManager()

我们来看几个示例,以展示 fragment 及其宿主和每个 fragment 相关联的 FragmentManager 实例之间的关系:

两个UI布局示例,显示了 fragment 及其宿主 activity 之间的关系

绿色代表宿主 activity

蓝色代表宿主 fragment

白色代表子 fragment

上图显示了两个示例,每个示例都有一个 activity 宿主。在这两个示例中,宿主 activity 均以 BottomNavigationView 的形式向用户显示顶级导航,该视图负责将 host fragment 替换为 app 中的不同界面,每个界面都是独立的 fragment。

Example 1 中的 host fragment 管理着两个子 fragment,这两个 fragment 构成了一个左右分离的两个界面。

Example 2 中的 host fragment 管理着一个单独的 子 fragment,它构成了滑动视图显示的 fragment。

基于上述设定,您可以认为每个宿主都关联着一个 FragmentManager 来管理其子 fragment。下图对此进行了说明:

每个宿主都关联着一个 FragmentManager 用于管理其子 fragment

使用合适的 FragmentManager 取决于调用者所在 fragment 层次结构中的位置以及您想要访问的 fragment manager。

一旦有了 FragmentManager 的引用,便可以使用它来操作显示给用户的 fragment。

子 fragment

一般而言,您的 app 应由一个或少量 activity 构成,每个 activity 代表一组相关的界面。Activity 可能提供 顶级导航,ViewModel 和 其它 fragment 间的 view-state。app 中每个独立的 目的地(destination)应该由一个 fragment 表示。

如果想要一次显示多个 fragment(如在一个拆分视图或仪表板中),则应使用由 destination fragment 及其 childFragmentManager 管理的 子 fragment。

其它使用子 fragment 的场景可能是:

  • Screen slides,父 fragment 使用 ViewPager2 管理着一系列子 fragment
  • 一组相关界面的子导航
  • Jetpack Navigation 使用子 fragment 作为独立目的地。但用户浏览您的 app 时,一个 activity 管理着一个单独的 parent NavHostFragment 并且使用不同的子 destination fragment 填充其 parent 的位置。

使用 FragmentManager

FragmentManager 管理着 fragment 的返回栈。在运行时,FragmentManager 可以执行返回栈操作(例如响应用户操作而添加/移除 fragment)。每组更改作为一个被称为 FragmentTransaction 的独立单元一起提交。有关事务更深入的讨论,请参见下一节。

但用户按下设备上的返回键时,或者当开发者调用 FragmentManager.popBackStack() 时,最顶部的 fragment 事务将从栈中弹出。换句话说,事务被撤销。如果栈中没有更多的 fragment 事务并且没有使用 子 fragment,那么返回事件将传递给 activity。如果使用了子 fragment,请参阅 子 fragment 和兄弟 fragment 的注意事项 一节。

在事务调用 addToBackStack() 时,请注意,该事务可以包含任意数量的操作,如添加多个 fragment,替换多个容器中的 fragment 等等。当返回栈弹出时,所有的这些操作都将作为单独的原子操作被撤销。如果在 popBackStack() 调用之前已经提交了其它事务,并且未对事务使用 addToBackStack(),则这些操作将不会撤销。因此,在一个 FragmentTransaction 中,请避免将影响返回栈的事务与不影响的事务混合使用。

执行事务

要在布局容器中显示一个 fragment,请使用 FragmentManager 创建 FragmentTransaction。然后,在事务中,您可以在容器上执行 add()replace() 操作。

一个简单的事务可能如下示例:

点击查看代码详情
// Kotlin
supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack("name") // name 可以为 null
}

// Java
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack("name") // name 可以为 nulll
    .commit();

在上面的示例中,ExampleFragment 替换了当前由 R.id.fragment_container ID 标识的布局容器中的 fragment(如果有)。将 fragment class 提供 replace() 方法允许 FragmentManager 使用其 FragmentFactory 处理实例化。有关更多信息,请参考下节。

setReorderingAllowed(true) 优化事务中涉及的 fragment 的状态更改,以便动画和过渡正常工作。有关使用动画和过渡进行导航的更多信息,请参见 Fragment 事务一节和转场动画一节。

调用 addToBackStack() 会将事务提交到返回栈。用户稍后可以撤回事务并通过安返回按钮返回上一个 fragment。如果您在单个事务添加或移除了多个fragment,则弹出返回栈时,所有这些操作都将被撤销。addToBackStack() 提供的可选名称使您能够使用 popBackStack() 弹出该特定事务。

如果在执行移除 fragment 事务时未调用 addToBackStack() ,这提交事务后被移除的 fragment 将被销毁(destroyed),并且用户无法导航回该 fragment。如果在删除 fragment 时调用了 addToBackStack(),则该 fragment 仅处于 STOPPED 状态,稍后当用户向返回时处于 RESUMED 状态。请注意,在这种情况下,其 view 已经 destroyed。(译者注:就是执行了 onDestroyView() 但没执行 onDestroy() 有关更多信息,请参见生命周期一节。

寻找一个已存在的 fragment

您可以使用 findFragmentById() 获得对布局容器中当前 fragment 的应用。使用 findFragmentById() 从 XML inflate 时或在 FragmentTransaction 中添加时通过给定 ID 来查找 fragment。下面是一个示例:

点击查看代码详情
// 👇 Kotlin
supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack(null)
}

...

val fragment: ExampleFragment =
        supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment


// 👇 Java
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();

...

ExampleFragment fragment =
        (ExampleFragment) fragmentManager.findFragmentById(R.id.fragment_container);

此外,您可以为 fragment 分配唯一的标签,并使用 findFragmentByTag() 获取引用。您可以在布局内定义的 fragment 上使用 android:tag XML 属性或在 FragmentTransaction 中的 add() 或 replace() 操作时分配标签。

点击查看代码详情
// 👇 Kotlin
supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container, "tag")
   setReorderingAllowed(true)
   addToBackStack(null)
}

...

val fragment: ExampleFragment =
        supportFragmentManager.findFragmentByTag("tag") as ExampleFragment


// 👇 Java
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null, "tag")
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();

...

ExampleFragment fragment = (ExampleFragment) fragmentManager.findFragmentByTag("tag");

子 fragment 和兄弟 fragment 的注意事项

在任何给定时间,仅允许一个 FragmentManager 来控制 fragment 的返回栈。如果您的 app 同时在屏幕上显示多个同级 fragment,或者您的 app 使用了子 fragment,则必须指定一个 FragmentManager 来处理 app 的主导航。

在 fragment 事务中定义主导航,请在事务上调用 setPrimaryNavigationFragment() 方法并传入拥有 childFragmentManager 主控制权的 fragment 实例。

将导航结构看作一系列层,activity 作为最外层,将子 fragment 的每一层包裹在下面。每一层都必须有一个主导航 fragment。但发生返回事件时,最内层控制导航行为,一旦最内层不再有要从返回栈中弹出的事务,控制权就会回到下一层,然后重复此过程,直到事件到达 activity 。

请注意,当同时显示两个或多个 fragment 时,其中只有一个可以是主导航 fragment。将 fragment 设置为主导航会删除上一个 fragment designation。使用上面的例子,如果设置 detail fragment 作为主导航 fragment,那么main fragment 的 designation 会被移除。

提供 fragment 的依赖

添加 fragment 时,可以手动实例化 fragment 并将其添加到 FragmentTransaction 中。

点击查看代码详情
// 👇 Kotlin
fragmentManager.commit {
    // 在 add 前实例化
    val myFragment = ExampleFragment()
    add(R.id.fragment_view_container, myFragment)
    setReorderingAllowed(true)
}


// 👇 Java
// 在 add 前实例化
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
    .add(R.id.fragment_view_container, myFragment)
    .setReorderingAllowed(true)
    .commit();

提交 fragment 事务时,创建的 fragment 实例就是所使用的实例。但是在系统资源回收或配置发生变化时,您的 activity 及其所有 fragment 都会被销毁,然后使用最适合的 Android 资源进行重新创建。FragmentManager 为您处理所有的这一切。它重新创建 fragment 的实例,然后将其 attach 到宿主并重新创建返回栈。

默认情况下,FragmentManager 使用框架提供的 FragmentFactory 实例化 fragment 的新实例。此默认工厂使用反射为 fragment 查找和调用无参构造器。这意味着您不能使用此默认工厂来提供对 fragment 的依赖性。这也意味着默认情况下,在重新创建过程中不会使用您用于首次创建 fragment 的任何自定义构造器。

若要提供对 fragment 的依赖关系或使用任何自定义构造器,必须创建 FragmentFactory 子类,然后重写 FragmentFactory.instantiate。接着您可以使用自定义工厂重写 FragmentManager 的默认工厂,随后将其用于实例化 fragment。

假设您有一个 DessertsFragment,负责在您的家乡展示受欢迎的甜点。假设 DessertsFragment 依赖于 DessertsRepository 类,该类为其提供向用户显示正确的 UI 所需的信息。

您可能定义在 DessertsFragment 的构造器中请求 DessertsRepository 实例:

点击查看代码详情
// 👇 Kotlin
class DessertsFragment(val dessertsRepository: DessertsRepository) : Fragment() {
    ...
}


// 👇 Java
public class DessertsFragment extends Fragment {
    private DessertsRepository dessertsRepository;

    public DessertsFragment(DessertsRepository dessertsRepository) {
        super();
        this.dessertsRepository = dessertsRepository;
    }

    ...
}

FragmentFactory 的简单实现可能类似如下内容:

点击查看代码详情
// 👇 Kotlin
class MyFragmentFactory(val repository: DessertsRepository) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
            when (loadFragmentClass(classLoader, className)) {
      																	//👇 此处更改了默认实现
                DessertsFragment::class.java -> DessertsFragment(repository)
                else -> super.instantiate(classLoader, className)
            }
}


// 👇 Java
public class MyFragmentFactory extends FragmentFactory {
    private DessertsRepository repository;

    public MyFragmentFactory(DessertsRepository repository) {
        super();
        this.repository = repository;
    }

    @NonNull
    @Override
    public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
        Class<? extends Fragment> fragmentClass = loadFragmentClass(classLoader, className);
        if (fragmentClass == DessertsFragment.class) {
          	//👇 此处更改了默认实现
            return new DessertsFragment(repository);
        } else {
            return super.instantiate(classLoader, className);
        }
    }
}

上面的示例创建了 FragmentFactory 的子类 MyFragmentFactory 。该类重写了 instantiate() 方法以为 DessertsFragment 提供自定义 fragment 创建逻辑,其它 fragment 由 FragmentFactory 的默认行为通过 super.instantiat() 处理。

然后,可以通过在 FragmentManager 上设置属性,将 MyFragmentFactory 指定为构造 app fragment 时要使用的工厂。您必须先在 activity 的 super.onCreate() 之前设置该属性,以确保在重新创建片段时使用 MyFragmentFactory

点击查看代码详情
// 👇 Kotlin
class MealActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = MyFragmentFactory(DessertsRepository.getInstance())
        super.onCreate(savedInstanceState)
    }
}


// 👇 Java
public class MealActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        DessertsRepository repository = DessertsRepository.getInstance();
        getSupportFragmentManager().setFragmentFactory(new MyFragmentFactory(repository));
        super.onCreate(savedInstanceState);
    }
}

请注意,在 activity 设置 FragmentFactory 会重写整个 activity 的 fragment 层次结构中的 fragment 创建。换句话说,您添加的任何子 fragment 的 childFragmentManager 均使用此处设置的自定义 fragment 工厂,除非在较低的级别被重写。

使用 FragmentFactory 测试

在单一 activity 体系结构中,应该使用 FragmentScenario 隔离地测试 fragment。 由于您不能依赖于 activity 的自定义 onCreate 逻辑,因此可以将 FragmentFactory 作为参数传递给 fragment 测试,如下:

点击查看代码详情
// 测试内部
val dessertRepository = mock(DessertsRepository::class.java)
launchFragment<DessertsFragment>(factory = MyFragmentFactory(dessertRepository)).onFragment {
    // 测试 Fragment 逻辑
}

有关测试过程的详细信息以及完整示例,请参考测试一节。

事务

在运行时,FragmentManager 可以添加/移除/替换 fragment 并执行其它操作以响应用户的交互。开发者提交的每组对 fragment 的更改被称为「事务」。您可以使用 FragmentTransaction 提供对 API 指定在事务内执行的操作,您可以将多个操作组织到一个事务中。例如,一个事务可以添加或替换多个 fragment。当您在同一个屏幕上显示多个同级 fragment 时,这十分有用。

您可以将每个事务保存到 FragmentManager 管理的返回栈中,从而允许用户在返回上一次 fragment 的状态,类似于从一个 activity 可以返回到之前的 activity。

您可以通过调用 FragmentManagerbeginTransaction() 方法获得一个 FragmentTransaction 实例,如下面的例子:

点击查看代码详情
// 👇 Kotlin
val fragmentManager = ...
val fragmentTransaction = fragmentManager.beginTransaction()

// 👇 Java
FragmentManager fragmentManager = ...
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

每个 FragmentTransaction 最后都必须提交事务。commit() 方法向 FragmentManager 发出信号,表示所有操作均已添加到事务中。

点击查看代码详情
// 👇 Kotlin
val fragmentManager = ...
// 这是由 fragment-ktx 提供的扩展函数
// 自动开启并提交事务
fragmentManager.commit {
    // 在这加入操作
}

// 👇 Java
FragmentManager fragmentManager = ...
FragmentTransaction fragmentTransaction = fragmentManager.beginTransaction();

// 在这加入操作

fragmentTransaction.commit();

允许 fragment 状态变化的重新排序

每个 FragmentTransaction 应该使用 setReorderingAllowed(true)

点击查看代码详情
// 👇 Kotlin
supportFragmentManager.commit {
    ...
    setReorderingAllowed(true)
}


// 👇 Java
FragmentManager fragmentManager = ...
fragmentManager.beginTransaction()
    ...
    .setReorderingAllowed(true)
    .commit();

为了兼容,默认不开启重排序。但是如果需要允许 FragmentManager 在返回栈上运行并运行动画和过渡时能够正确执行 FragmentTransaction,启用重排序可确保一起执行多个事务时,任何中间 fragment(如添加并立即替换的中间 fragment)都不会经历生命周期的更改或执行其它动画或过渡。请注意,重排序影响了事务的开启和事务的撤销。

添加/移除 fragment

要将 fragment 添加到 FragmentManager,请在事务上调用 add() 方法。该方法接收 fragment 容器的 ID 和待添加 fragment 的类名。添加的 fragment 将移至 RESUMED 状态。强烈建议容器使用 FragmentContainerView。

要从宿主中移除 fragment,请调用 remove() 方法并传入一个 fragment 实例。该 fragment 实例是通过 findFragmentById()findFragmentByTag 从 fragment manager 中检索到的。如果 fragment 已经添加到容器中,则其视图也将从容器中移除。被移除的 fragment 将移至 DESTROYED 状态。

使用 replace() 将新的 fragment 实例替换容器中现有的 fragment。调用 replace() 等效于对容器中的一个 fragment 调用 remove() 并将一个新的 fragment 添加到同一容器中。

以下代码显示了如果使用一个 fragment 替换另一个 fragment:

点击查看代码详情
// 👇 Kotlin
val fragmentManager = // ...

fragmentManager.commit {
    setReorderingAllowed(true)

    replace<ExampleFragment>(R.id.fragment_container)
}


// 👇 Java
FragmentManager fragmentManager = ...
FragmentTransaction transaction = fragmentManager.beginTransaction();
transaction.setReorderingAllowed(true);

transaction.replace(R.id.fragment_container, ExampleFragment.class, null);

transaction.commit();

在上面示例中,ExampleFragment 的新实例将替换当前由 R.id.fragment_container 标识的布局容器的 fragment(如果有)。

🌟 注意:强烈推荐操作 fragment 时使用 Class 而不是 fragment 实例以确保 fragment 通过 saved state 恢复时使用相同的机制。有关更多详细信息,请参见 Fragment manager 一节。

默认情况下,在 FragmentTransaction 中所做的更改不会添加的返回栈中,要保存这些更改,可以在 FragmentTransaction 上调用 addToBackStack() 方法。有关更多信息,请参考 Fragment manager 一节。

异步提交

调用 commit() 不会立即执行事务,该事务会被安排为能够在主线程上尽快运行。如果需要可以调用 commitNow() 立即在主线程上运行 fragment 事务。

请注意,commitNowaddToBackStack 不兼容。不过您可以通过调用 executePendingTransactions() 执行已经调用 commit() 方法但没运行的事务,该方法与addToBackStack 兼容。

对于绝大多数场景,使用 commit() 即可。

操作顺序很重要

FragmentTransaction 中执行操作的顺序很重要,尤其是在使用 setCustomAnimations() 方法时。 此方法将给定的动画作用给稍后所有的 fragment 操作。

点击查看代码详情
// 👇 Kotlin
supportFragmentManager.commit {
    setCustomAnimations(enter1, exit1, popEnter1, popExit1)
    add<ExampleFragment>(R.id.container) // 第一个动画
    setCustomAnimations(enter2, exit2, popEnter2, popExit2)
    add<ExampleFragment>(R.id.container) // 第二个动画
}


// 👇 Java
getSupportFragmentManager().beginTransaction()
        .setCustomAnimations(enter1, exit1, popEnter1, popExit1)
        .add(R.id.container, ExampleFragment.class, null) // 第一个动画
        .setCustomAnimations(enter2, exit2, popEnter2, popExit2)
        .add(R.id.container, ExampleFragment.class, null) // 第二个动画
        .commit()

限制 fragment 的生命周期

FragmentTransaction 可以影响在事务范围内添加的各个 fragment 的生命周期状态。创建 FragmentTransaction 时,setMaxLifecycle() 方法可以为给定 fragment 设置最大状态。例如, ViewPager2 使用 setMaxLifecycle() 方法限制屏幕外的 fragment 为 STARTED 状态。

显示和隐藏 fragment 的 view

使用 FragmentTransaction 的 show() 和 hide() 方法来显示和隐藏已添加到容器的 fragment 的 view。这些方法设置 fragment view 的可见性而不影响 fragment 的生命周期。

尽管您不需要使用 fragment 事务来切换 fragment view 的可见性,但是这些方法对于改变返回栈上事务的可见性的场景很有用。

连接和分离 fragment

FragmentTransactiondetach() 方法将 fragment 与 UI 分离,销毁 fragment 的视图。该 fragment 保持与返回栈中相同的状态(STOPPED)。这意味着 fragment 已从 UI 中删除,但仍由 fragment manager 管理。

attach() 方法重新连接之前分离的 fragment。这将导致其视图树重新创建,添加到 UI 上并显示。

由于将 FragmentTransaction 视为单个原子操作集,因此在同一事务中对分离和连接到同一 fragment 实例的调用彼此间进行了抵消,从而避免了 fragment UI 的销毁和立即重建。如果要分离然后立即重新连接 fragment,请使用单独的事务,如果使用了 commit() 提交事务,则使用 executePendingOperations() 方法进行分离。

🌟 注意attach()detach() 方法与 Fragment 的 onAttach()onDetach() 方法无关。有关这些 Fragment 方法的更多信息,参考生命周期一节。

转场动画

Fragment API 提供了两种在连接 fragment 导航的切换效果。一个是 Animation 框架,包括 AnimationAnimator。另一个是 Transition 框架,包含共享元素转换。

🌟 注意:在本节中,我们使用 animation 来描述 Animation 框架中的效果,使用 transition 来描述 Transition 框架的效果。这两个框架是互斥的,不应同时使用。

您可以将自定义效果看成进入和退出 fragment 以及 fragment 共享元素的过渡效果。

  • 进入效果定义了 fragment 如何进入屏幕。如您可以创建一种 fragment 进入时从屏幕边缘滑入的效果。
  • 退出效果定义了 fragment 如何退出屏幕。如您可以创建一种 fragment 离开时淡出的效果。
  • 共享元素过渡定义了两个 fragment 间共享的视图如何在它们之间移动。如一旦 B 变为可见,显示在 fragment A 的 ImageView 中的图像便会转换为 fragment B。

设置 animation

首先,您需要为进入和退出效果创建动画,这些动画将在导航到新 fragment 时运行。 您可以将动画定义为补间动画资源。 这些资源使您可以定义动画期间 fragment 应如何旋转,拉伸,淡入淡出和移动。 例如,您可能希望当前 fragment 淡出,而新 fragment 从屏幕的右边缘滑入,如下图:

这些动画可以在 res/anim 目录中定义:

点击查看代码详情
<!-- res/anim/fade_out.xml -->
<?xml version="1.0" encoding="utf-8"?>
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="1"
    android:toAlpha="0" />

<!-- res/anim/slide_in.xml -->
<?xml version="1.0" encoding="utf-8"?>
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="100%"
    android:toXDelta="0%" />

🌟 注意:强烈建议对涉及多种动画类型的效果使用 transition ,因为使用嵌套 AnimationSet 存在已知问题。

您还可以为弹出返回栈时运行的进入和退出效果指定动画。 这些被称为 popEnterpopExit 动画。 例如,当用户跳回到上一个屏幕时,您可能希望当前 fragment 滑出屏幕的右边缘,而前一个 fragment 淡入:

这些动画可以这样定义:

点击查看代码详情
<!-- res/anim/slide_out.xml -->
<translate xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromXDelta="0%"
    android:toXDelta="100%" />

<!-- res/anim/fade_in.xml -->
<alpha xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:interpolator="@android:anim/decelerate_interpolator"
    android:fromAlpha="0"
    android:toAlpha="1" />

定义动画后,可以通过调用 FragmentTransaction.setCustomAnimations() 来使用它们并通过其资源 id 传递动画资源,如下示例:

点击查看代码详情
// 👇 Kotlin
val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(
        enter = R.anim.slide_in,
        exit = R.anim.fade_out,
        popEnter = R.anim.fade_in,
        popExit = R.anim.slide_out
    )
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

// 👇 Java
Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(
        R.anim.slide_in,  // enter
        R.anim.fade_out,  // exit
        R.anim.fade_in,   // popEnter
        R.anim.slide_out  // popExit
    )
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

🌟 注意FragmentTransaction.setCustomAnimations() 将自定义动画应用于 FragmentTransaction 中所有未来的 fragment 操作。事务中之前的操作不受影响。

设置 transition

您也可以使用 transition 来定义进入和退出效果。 可以在 XML 资源文件中定义这些 transition。例如,您可能希望当前 fragment 淡出,而新 fragment 从屏幕的右边缘滑入。 这些 transition 可以定义如下:

点击查看代码详情
<!-- res/transition/fade.xml -->
<fade xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"/>

<!-- res/transition/slide_right.xml -->
<slide xmlns:android="http://schemas.android.com/apk/res/android"
    android:duration="@android:integer/config_shortAnimTime"
    android:slideEdge="right" />

定义 transition 后,通过在进入 fragment 调用 setEnterTransition() 以及在退出 fragment 调用 setExitTransition() 来应用 transition 并通过其资源 id 传递资源,如下所示:

点击查看代码详情
// 👇 Kotlin
class FragmentA : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        exitTransition = inflater.inflateTransition(R.transition.fade)
    }
}

class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val inflater = TransitionInflater.from(requireContext())
        enterTransition = inflater.inflateTransition(R.transition.slide_right)
    }
}

// 👇 Java
public class FragmentA extends Fragment {
    @Override
    public View onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setExitTransition(inflater.inflateTransition(R.transition.fade));
    }
}

public class FragmentB extends Fragment {
    @Override
    public View onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        TransitionInflater inflater = TransitionInflater.from(requireContext());
        setEnterTransition(inflater.inflateTransition(R.transition.slide_right));
    }
}

Fragment 支持 AndroidX transitions。尽管 Fragment 也支持 framework transitions,但我们强烈建议使用 AndroidX transitions,因为它支持 API 14 及以上版本并包含了一些错误修复。

使用共享元素过渡

作为 transition 框架的一部分,共享元素过渡决定了 fragment 切换期间相应视图如何在两个 fragment 之间移动。 例如,您可能希望一旦 B 变得可见,在 fragment A 的 ImageView 中显示的图像就过渡到 fragment B。如下图:

这是使用共享元素进行 fragment 切换的步骤:

  1. 为每个共享元素视图分配一个唯一的 transition 名称
  2. 将共享元素视图和 transition 名称添加到 FragmentTransaction
  3. 设置共享元素过渡动画

首先,必须为每个共享元素视图分配唯一的 transition 名称以允许将 view 从一个 fragment 映射到下一个 fragment。使用 ViewCompat.setTransitionName() 在每个 fragment 布局中的共享元素上设置 transition 名称,该方法可兼容 API 14 及以上的版本。例如,fragment A 和 fragment B 中的 ImageView 的 transition 可以这样分配:

点击查看代码详情
// 👇 Kotlin
class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val itemImageView = view.findViewById<ImageView>(R.id.item_image)
        ViewCompat.setTransitionName(itemImageView, “item_image”)
    }
}

class FragmentB : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        ...
        val heroImageView = view.findViewById<ImageView>(R.id.hero_image)
        ViewCompat.setTransitionName(heroImageView, “hero_image”)
    }
}

// 👇 Java
public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView itemImageView = view.findViewById(R.id.item_image);
        ViewCompat.setTransitionName(itemImageView, “item_image”);
    }
}

public class FragmentB extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        ...
        ImageView heroImageView = view.findViewById(R.id.hero_image);
        ViewCompat.setTransitionName(heroImageView, “hero_image”);
    }
}

🌟 注意:对于minSdkVersion API 21 或更高版本的 app,可以选择使用 XML 布局内的 android:transitionName 属性将 transition 名称分配给特定视图。

要将共享元素加入到 fragment 切换中,您的 FragmentTransaction 必须知道每个共享元素的视图如何从一个 fragment 映射到下一个 fragment。通过调用 FragmentTransaction.addSharedElement() 将每个共享元素添加到 FragmentTransaction,在下一个 fragment 中传入视图和相应视图的 transition 名称,如下:

点击查看代码详情
// 👇 Kotlin
val fragment = FragmentB()
supportFragmentManager.commit {
    setCustomAnimations(...)
    addSharedElement(itemImageView, “hero_image”)
    replace(R.id.fragment_container, fragment)
    addToBackStack(null)
}

// 👇 Java
Fragment fragment = new FragmentB();
getSupportFragmentManager().beginTransaction()
    .setCustomAnimations(...)
    .addSharedElement(itemImageView, “hero_image”)
    .replace(R.id.fragment_container, fragment)
    .addToBackStack(null)
    .commit();

要指定共享元素如何从一个 fragment 过渡到下一个 fragment,必须在要导航到的 fragment 上设置 enter transition。 调用 fragment 的 onCreate() 方法中的 Fragment.setSharedElementEnterTransition(),如下:

点击查看代码详情
// 👇 Kotlin
class FragmentB : Fragment() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        sharedElementEnterTransition = TransitionInflater.from(requireContext())
             .inflateTransition(R.transition.shared_image)
    }
}

// 👇 Java
public class FragmentB extends Fragment {
    @Override
    public View onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        Transition transition = TransitionInflater.from(requireContext())
            .inflateTransition(R.transition.shared_image);
        setSharedElementEnterTransition(transition);
    }
}

shared_image transition 定义如下:

点击查看代码详情
<!-- res/transition/shared_image.xml -->
<transitionSet>
    <changeImageTransform />
</transitionSet>

所有 Transition 的子类都支持共享元素的过渡。如果你想要创建一个自定义的 Transition,参见 创建自定义转场动画。上一个示例中使用的 changeImageTransform 是一个定义好的转换。 您可以在 Transition 的 API 文档中找到其它 Transition 的子类。

默认情况下,共享元素的 enter transition 也用作共享元素的 return transition。 return transition 确定了 fragment 事务从返回栈中弹出时共享元素如何转换回上一个 fragment。 如果要指定其它返回转换,则可以在 fragment 的 onCreate() 方法中使用 Fragment.setSharedElementReturnTransition() 进行指定。

延迟过渡

在某些情况下,您可能需要将 fragment 转换延迟一小段时间。 例如,您可能需要等到对进入的 fragment 中的所有视图进行测量和布局后,系统才能准确捕捉其过渡的开始和结束状态。

此外,您的 transition 可能需要延迟直到加载了一些必要的数据。例如在共享元素加了图像之前需要一直等待,否则如果图像在过渡期间或过渡之后完成加载,过渡可能会受到干扰。

要延迟过渡,必须确保 fragment 事务允许 fragment 状态更改的重排序。请调用 FragmentTransaction.setReorderingAllowed(),如下:

要延迟转换,在进入 fragment 的 onViewCreated() 方法中调用 Fragment.postponeEnterTransition()

加载数据并准备开始转换后,请调用 Fragment.startPostponedEnterTransition() 方法。 以下示例使用 Glide 库将图像加载到共享的 ImageView 中,将相应的 transition 推迟到图像加载完成。

在处理网速慢的情况时,您可能需要延迟一定时间后才开始过渡,而不是等待所有数据加载完毕再开始。 在这种场景下,您可以改为在进入 fragment 的 onViewCreated() 方法中调用 Fragment.postponeEnterTransition(long, TimeUnit) 并传入持续时间和时间单位。 经过指定的时间后,过渡将自动开始。

在 RecyclerView 中使用共享元素过渡

在测量并布局了进入 fragment 中的所有视图之前,不应开始延迟的 enter transition。 使用 RecyclerView 时,必须等待任何数据加载并准备好绘制 RecyclerView item 再开始 transition。 这是一个例子:

点击查看代码详情
// 👇 Kotlin
class FragmentA : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        postponeEnterTransition()

        // 等待数据加载
        viewModel.data.observe(viewLifecycleOwner) {
            // 在 RecyclerView adapter 上设置数据
            adapter.setData(it)
            // 所有 view 测量和布局完毕后开始 transition
            (view.parent as? ViewGroup)?.doOnPreDraw {
                startPostponedEnterTransition()
            }
        }
    }
}

// 👇 Java
public class FragmentA extends Fragment {
    @Override
    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        postponeEnterTransition();

        final ViewGroup parentView = (ViewGroup) view.getParent();
        // 等待数据加载
        viewModel.getData()
            .observe(getViewLifecycleOwner(), new Observer<List<String>>() {
                @Override
                public void onChanged(List<String> list) {
                    // 在 RecyclerView adapter 上设置数据
                    adapter.setData(it);
                    // 所有 view 测量和布局完毕后开始 transition
                    parentView.getViewTreeObserver()
                        .addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
                            parentView.getViewTreeObserver()
                                .removeOnPreDrawListener(this);
                            startPostponedEnterTransition();
                            return true;
                        });
                }
        });
    }
}

请注意,在 fragment 视图的父级上设置了 ViewTreeObserver.OnPreDrawListener。 这是为了确保在开始延迟 enter transition 之前,已测量并布置了所有 fragment 的视图并准备好绘制。

🌟 注意:从使用 RecyclerView 的 fragment 使用共享元素过渡到另一个 fragment 时,您仍然 必须 使用 RecyclerView 延迟该 fragment,以确保返回的共享元素过渡在弹出到 RecyclerView 时能够正常运行。

将共享元素转换与 RecyclerView 一起使用时要考虑的另一点是,由于任意数量的 item 共享该布局,因此无法在 RecyclerView item 的 XML 布局中设置 transition 名称。 必须分配唯一的 transition 名称,以便过渡动画使用正确的视图。

通过绑定 ViewHolder,可以为每个 item 的共享元素赋予唯一的 transition 名称。 例如,如果每个 item 的数据都包含唯一的 ID,则可以将其用作 transition 名称,如下示例:

点击查看代码详情
// 👇 Kotlin
class ExampleViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
    val image = itemView.findViewById<ImageView>(R.id.item_image)

    fun bind(id: String) {
        ViewCompat.setTransitionName(image, id)
        ...
    }
}

// 👇 Java
public class ExampleViewHolder extends RecyclerView.ViewHolder {
    private final ImageView image;

    ExampleViewHolder(View itemView) {
        super(itemView);
        image = itemView.findViewById(R.id.item_image);
    }

    public void bind(String id) {
        ViewCompat.setTransitionName(image, id);
        ...
    }
}

额外资源

要了解有关片段过渡的更多信息,请参见以下其他资源。

示例

博客

生命周期

每个 Fragment 实例都有其自己的生命周期。 当用户导航并与您的 app 交互时(如添加,移除以及进入或退出屏幕),您的 fragment 会在其生命周期中的各种状态之间转换。

为了管理生命周期,Fragment 实现了 LifecycleOwner,公开了可以通过 getLifecycle() 方法获取 Lifecycle 对象。

每个可能的生命周期状态都在 Lifecycle.State 枚举中表示:

通过在 Lifecycle 之上构建 Fragment,您可以使用 可感知生命周期组件 处理生命周期。例如,您可以使用生命周期感知组件在屏幕上显示设备的位置。 当 fragment 变为活跃状态时,此组件可以自动开始监听,而当 fragment 变为非活跃状态时,该组件可以停止。

作为使用 LifecycleObserver 的替代方法,Fragment 类包括与 fragment 生命周期中每个更改相对应的回调方法。这些方法是:onCreate()onStart()onResume()onPause()onStop()onDestroy()

fragment 的 view 具有单独的生命周期,该生命周期独立于 fragment 的生命周期进行管理(译者注:官方正在考虑将二者合二为一)。fragment 为 view 维护着一个 LifecycleOwner,它可以通过 getViewLifecycleOwner()getViewLifecycleOwnerLiveData() 对其进行访问。有权访问视图的生命周期对于仅在 fragment view 存在时执行工作的生命周期感知组件十分有用,例如观察 LiveData 仅在屏幕显示时执行工作。

本节详细讨论的 fragment 的生命周期,解释了确定 fragment 生命周期状态的一些规则,并展示了生命周期状态与 fragment 生命周期回调之间的关系。

Fragment 和 fragment manager

当 fragment 被实例化时,它以 INITIALIZED 状态开始。 为了使 fragment 在其整个生命周期中切换,必须将其添加到 FragmentManager。 FragmentManager 负责确定其 fragment 应处于什么状态,然后将其移入该状态。

在 fragment 生命周期之外,FragmentManager 还负责将 fragment attach 到其 宿主 activity,并在不再使用该 fragment 时将其 detach。 Fragment 类具有两个回调方法 onAttach()onDetach(),当这些事件中的任何一个发生时,您都可以重写它们以执行自己的逻辑。

将 fragment 添加到 FragmentManager 并将其 attach 到其宿主 activity 时,将调用 onAttach() 回调,fragment 处于活跃状态,并且 FragmentManager 正在管理其生命周期状态。 此时,FragmentManager 方法(如 findFragmentById())将返回此 fragment。

onAttach() 方法永远在任何生命周期状态改变前调用。

当 fragment 已从 FragmentManager 中移除并与其宿主 activity detach 时,将调用 onDetach() 回调。 该 fragment 不再活跃,无法再使用 findFragmentById() 进行检索。

onDetach() 方法永远在任何生命周期状态改变后调用。

请注意:这些回调与 FragmentTransaction 的 attach() 和 detach() 方法无关。有关这些方法的更多信息,请参见 Fragment 事务一节。

⚠️ 警告避免从 FragmentManager 中移除 Fragment 实例后重用它们 当 fragment 处理自己的内部状态清除时,您可能会无意间将自己的状态转移到重用实例中。

Fragment 生命周期状态和回调

在确定 fragment 的生命周期状态时,FragmentManager 考虑以下因素:

  • fragment 的最大状态由其 FragmentManager 决定。 fragment 无法超出其 FragmentManager 的状态
  • 作为 FragmentTransaction 的一部分,您可以使用 setMaxLifecycle() 在 fragment 上设置最大生命周期状态
  • fragment 的生命周期状态永远不能大于其宿主。 例如,父 fragment 或 activity 必须在其子 fragment 之前 start。 同样,子 fragment 必须在其父 fragment 或 activity 之前 stop

⚠️ 警告避免在 XML 中使用 <fragment> 标签添加 fragment因为 <fragment> 标签允许 fragment 移出其 FragmentManager 的状态。 请始终使用 FragmentContainerView 通过 XML 添加 fragment。

上图显示了每个 fragment 的生命周期状态,以及它们与 fragment 的生命周期回调和 fragment 的视图生命周期之间的关系。

随着 fragment 在其生命周期中的切换,它会在其状态之间上下移动。 例如,添加到返回栈的顶部的 fragment 从 CREATED 向上移动到 STARTED 再到 RESUMED。 相反,当一个 fragment 从返回栈中弹出时,它将在这些状态中向下移动,从 RESUMEDSTARTED 再到 CREATED,最后到 DESTROYED

状态的向上转换

当向上移动其生命周期状态时,fragment 首先为其新状态调用关联的生命周期回调。 回调完成后,相关的 Lifecycle.Event 发出给观察者,然后由 fragment 的 view Lifecycle(如果已实例化)跟随。

Fragment CREATED

当您的 fragment 达到 CREATED 状态时,已将其添加到 FragmentManager 中并且已经调用了 onAttach() 方法。

这将是通过 fragment 的 SavedStateRegistry 恢复与 fragment 本身关联的所有保存状态的合适位置。 请注意,此时尚未创建 fragment 的 view,并且只有在创建 view 之后,才应还原与 fragment 的 view 关联的任何状态。

这个过程将 调用 onCreate() 回调。 回调还将接收一个 saveInstanceStateStateBundle 参数,其中包含先前由 onSaveInstanceState() 保存的状态。 请注意,第一次创建该 fragment 时,savedInstanceState 的值为 null,但 对于之后的重新创建,即使未重写 onSaveInstanceState,它也始终为非 null。 有关更多详细信息,请参见使状态保存一节。

Fragment CREATED 和 View INITIALIZED

仅当您的 fragment 提供有效的 View 实例时,才创建 fragment 的 view 生命周期。 在大多数情况下,您可以使用带有 @LayoutId 的 fragment 构造器,该构造器会在适当的时间自动 inflate view。 您还可以重写 onCreateView() 以编程方式 inflate 或创建 fragment 的 view。

当且仅当使用非 null view 实例化 fragment 的视图时,该视图才设置在 fragment 上,并且可以使用 getView() 进行检索。 然后,使用与 fragment 视图相对应的新的 INITIALIZED LifecycleOwner 更新 getViewLifecycleOwnerLiveData()。 此时也会 调用 onViewCreated() 生命周期回调

这里是设置视图初始状态位置,开始观察其回调更新 fragment 视图的 LiveData 实例以及在 fragment 视图中的任何 RecyclerViewViewPager2 实例上设置 adapter 的适当位置。

Fragment 和 View CREATED

创建 fragment 的视图之后,将还原先前的视图状态(如果有),然后将视图的生命周期移至 CREATED 状态。 视图生命周期所有者还向其观察者发出 ON_CREATE 事件。 在这里,您应该还原与 fragment 视图关联的所有其他状态。

此过程还将 调用 onViewStateRestored() 回调

Fragment 和 View STARTED

强烈建议将支持生命周期的组件绑定到 fragment 的 STARTED 状态,因为这种状态可以确保该 fragment 的视图可用(如果已创建),并且可以安全地对该 fragment 的子 FragmentManager 执行 FragmentTransaction 。 如果 fragment 的视图为非 null,则在 fragment 的生命周期移至 STARTED 后立即将 fragment 的视图 Lifecycle 移至 STARTED

当 fragment 变为 STARTED 状态时,将 调用 onStart() 回调

🌟 注意:诸如 ViewPager2 之类的组件将屏幕外 fragment 的最大生命周期设置为 STARTED

Fragment 和 View RESUMED

当 fragment 可见时,所有 AnimatorTransition 效果均已完成,并且该 fragment 已准备就绪,可以与用户进行交互。 fragment 的生命周期移至 RESUMED 状态,并调用 onResume() 回调

切换到 RESUMED 状态是指示用户现在可以与您的 fragment 进行交互的状态。 未 RESUMED 的 fragment 不应手动设置 view 的焦点或尝试 处理输入法的可见性

状态的向下转换

当 fragment 向下移动到较低的生命周期状态时,相关的 Lifecycle.Event 将通过 fragment 的 view Lifecycle(如果已实例化)发送给观察者,然后是 fragment 的 Lifecycle。 发出 fragment 的生命周期事件后,fragment 将调用关联的生命周期回调。

Fragment 和 View STARTED

随着用户开始离开 fragment 并且在 fragment 仍可见的时候,fragment 及其 view 的生命周期将移回到 STARTED 状态,并向其观察者发出 ON_PAUSE 事件。 然后,该 fragment 调用其 onPause() 回调

Fragment 和 View CREATED

一旦该 fragment 不再可见,该 fragment 及其 view 的生命周期便进入 CREATED 状态,并向其观察者发出 ON_STOP 事件。 状态转换不仅由父 activity 或 fragment 停止而触发,还由父 activity 或 fragment 保存状态而触发。此行为可确保在保存 fragment 状态之前调用 ON_STOP 事件。 这使 ON_STOP 事件成为在子 FragmentManager 上安全执行 FragmentTransaction 的最后位置。

如上图 所示,onStop() 回调的顺序以及使用 onSaveInstanceState() 保存状态的过程根据 API 级别而有所不同。 对于 API 28 之前的所有版本,在 onStop() 之前调用 onSaveInstanceState()。 对于 API 级别 28 及以后版本,调用顺序相反。

Fragment CREATED 和 View DESTROYED

在所有退出动画和过渡都完成并且 fragment 的 view 已从窗口中 detach 出来之后,fragment 的 view Lifecycle 被移到 DESTROYED 状态,并向其观察者发出 ON_DESTROY 事件。 然后,该 fragment 调用其 onDestroyView()回调。 此时,fragment 的 view 已到达其生命周期的尽头,并且 getViewLifecycleOwnerLiveData() 返回 null。

此时,应该删除对 fragment view 的所有引用,从而可以垃圾回收 fragment 的 view。

Fragment DESTROYED

如果移除了 fragment,或者 FragmentManager 被销毁,则 fragment 的生命周期将进入 DESTROYED 状态,并将 ON_DESTROY 事件发送给其观察者。 然后,该 fragment 调用其 onDestroy() 回调。 至此,该 fragment 已达到其生命周期的尽头。

额外资源

有关 fragment 生命周期的更多信息,请参见以下其他资源。

关于我

我是 Flywith24,Android App/Rom 层开发者。目前专注于 Android 体系化文章的写作。

Android Detail 专栏 正在更新中,想要建立系统化知识体系的小伙伴可以去看看哦。我的所有博客内容已经分类整理 在这里,点击右上角的 Watch 可以及时获取我的文章更新哦 😉