片段管理器

FragmentManager 是负责对应用的片段执行操作的类,例如添加、删除或替换它们以及将它们添加到后退栈。

如果您正在使用 Jetpack Navigation 库,则您可能永远不会直接与 FragmentManager 交互,因为它会代表您与 FragmentManager 协同工作。但是,任何使用片段的应用都在某种程度上使用 FragmentManager,因此了解它是什么以及它是如何工作的非常重要。

此页面涵盖以下内容

  • 如何访问 FragmentManager
  • FragmentManager 在与您的活动和片段相关的角色。
  • 如何使用 FragmentManager 管理后退栈。
  • 如何向您的片段提供数据和依赖项。

访问 FragmentManager

您可以从活动或片段访问 FragmentManager

FragmentActivity 及其子类(如 AppCompatActivity)可以通过 getSupportFragmentManager() 方法访问 FragmentManager

片段可以托管一个或多个子片段。在片段内部,您可以通过 getChildFragmentManager() 获取对管理片段子元素的 FragmentManager 的引用。如果您需要访问其主机 FragmentManager,则可以使用 getParentFragmentManager()

以下是一些示例,以了解片段、其主机以及与每个片段关联的 FragmentManager 实例之间的关系。

two ui layout examples showing the relationships between
            fragments and their host activities
图 1. 两个 UI 布局示例,显示了片段与其主机活动之间的关系。

图 1 显示了两个示例,每个示例都具有单个活动主机。这两个示例中的主机活动都向用户显示顶级导航,作为 BottomNavigationView,它负责用应用中的不同屏幕替换主机片段。每个屏幕都作为单独的片段实现。

示例 1 中的主机片段托管两个构成拆分视图屏幕的子片段。示例 2 中的主机片段托管一个构成 滑动视图 显示片段的单个子片段。

鉴于此设置,您可以将每个主机视为具有与其关联的 FragmentManager,该管理器管理其子片段。这在图 2 中进行了说明,以及 supportFragmentManagerparentFragmentManagerchildFragmentManager 之间的属性映射。

each host has its own FragmentManager associated with it
            that manages its child fragments
图 2. 每个主机都有其自己的 FragmentManager 与其关联,该管理器管理其子片段。

需要引用的适当的FragmentManager属性取决于调用位置在片段层次结构中的位置以及您尝试访问的片段管理器。

获得FragmentManager的引用后,您可以使用它来操作显示给用户的片段。

子片段

一般来说,您的应用由应用程序项目中一个或少量活动组成,每个活动代表一组相关的屏幕。活动可能提供一个放置顶级导航的点,以及在片段之间限定ViewModel对象和其他视图状态的位置。片段表示应用中的单个目标。

如果要同时显示多个片段,例如在分屏视图或仪表板中,可以使用由目标片段及其子片段管理器管理的子片段。

子片段的其他用例如下所示

  • 屏幕滑动,在父片段中使用ViewPager2管理一系列子片段视图。
  • 一组相关屏幕内的子导航。
  • Jetpack Navigation 使用子片段作为单个目标。活动承载单个父NavHostFragment,并在用户浏览应用时使用不同的子目标片段填充其空间。

使用 FragmentManager

FragmentManager管理片段回退栈。在运行时,FragmentManager可以执行回退栈操作,例如响应用户交互添加或删除片段。每个更改集都作为一个称为FragmentTransaction的单个单元提交。有关片段事务的更深入讨论,请参阅片段事务指南

当用户点击设备上的后退按钮,或者当您调用FragmentManager.popBackStack()时,最顶层的片段事务将从栈中弹出。如果栈上没有更多片段事务,并且您没有使用子片段,则后退事件会冒泡到活动。如果您*正在*使用子片段,请参阅子片段和同级片段的特殊注意事项

当您在事务上调用addToBackStack()时,事务可以包含任意数量的操作,例如在多个容器中添加多个片段或替换片段。

回退栈弹出时,所有这些操作都作为单个原子操作反转。但是,如果您在popBackStack()调用之前提交了其他事务,并且您*没有*为事务使用addToBackStack(),则这些操作*不会*反转。因此,在单个FragmentTransaction内,避免交错影响回退栈的事务与不影响回退栈的事务。

执行事务

要在布局容器内显示片段,请使用FragmentManager创建FragmentTransaction。在事务中,您可以对容器执行add()replace()操作。

例如,一个简单的FragmentTransaction可能如下所示

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack("name") // Name can be null
}

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack("name") // Name can be null
    .commit();

在此示例中,ExampleFragment替换当前位于由R.id.fragment_container ID标识的布局容器中的片段(如果有)。向replace()方法提供片段的类可以让FragmentManager使用其FragmentFactory处理实例化。有关更多信息,请参阅向片段提供依赖项部分。

setReorderingAllowed(true)优化事务中涉及的片段的状态更改,以便动画和过渡正常工作。有关使用动画和过渡导航的更多信息,请参阅片段事务使用动画在片段之间导航

调用addToBackStack()将事务提交到回退栈。用户以后可以通过点击后退按钮来反转事务并恢复以前的片段。如果您在一个事务中添加或删除了多个片段,则所有这些操作在回退栈弹出时都会撤消。addToBackStack()调用中提供的可选名称使您可以使用popBackStack()返回到特定事务。

如果您在执行删除片段的事务时没有调用addToBackStack(),则在事务提交时会销毁已删除的片段,并且用户无法导航回该片段。如果您在删除片段时确实调用了addToBackStack(),则片段仅STOPPED,并在用户导航返回时稍后RESUMED。在这种情况下,其视图*将*被销毁。有关更多信息,请参阅片段生命周期

查找现有片段

您可以使用findFragmentById()获取布局容器内当前片段的引用。使用findFragmentById()通过给定的 ID(从 XML 膨胀时)或通过容器 ID(在FragmentTransaction中添加时)查找片段。以下是一个示例

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);

或者,您可以为片段分配一个唯一的标签,并使用findFragmentByTag()获取引用。您可以使用片段的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");

子片段和同级片段的特殊注意事项

在任何给定时间,只有一个FragmentManager可以控制片段回退栈。如果您的应用同时在屏幕上显示多个同级片段,或者如果您的应用使用子片段,则一个FragmentManager被指定来处理应用的主要导航。

要在片段事务内定义主要导航片段,请在事务上调用setPrimaryNavigationFragment()方法,并传入其childFragmentManager具有主要控制权的片段的实例。

将导航结构视为一系列层,其中活动作为最外层,在其下方包装每一层子片段。每一层都有一个主要导航片段。

当发生后退事件时,最内层控制导航行为。一旦最内层没有更多可以弹出的片段事务,控制权将返回到下一层,此过程重复,直到到达活动。

当同时显示两个或更多片段时,其中只有一个是主要导航片段。将片段设置为主要导航片段会从之前的片段中删除指定。使用前面的示例,如果将详细信息片段设置为主要导航片段,则主片段的指定将被删除。

支持多个回退栈

在某些情况下,您的应用可能需要支持多个回退栈。一个常见的示例是,如果您的应用使用底部导航栏。FragmentManager允许您使用saveBackStack()restoreBackStack()方法支持多个回退栈。这些方法允许您通过保存一个回退栈并恢复另一个回退栈来在回退栈之间切换。

saveBackStack()的工作方式类似于使用可选的name参数调用popBackStack():指定的交易以及栈上其后的所有交易都将弹出。不同之处在于saveBackStack()保存弹出交易中所有片段的状态。

例如,假设您之前使用addToBackStack()提交FragmentTransaction将片段添加到回退栈,如下例所示

Kotlin

supportFragmentManager.commit {
  replace<ExampleFragment>(R.id.fragment_container)
  setReorderingAllowed(true)
  addToBackStack("replacement")
}

Java

supportFragmentManager.beginTransaction()
  .replace(R.id.fragment_container, ExampleFragment.class, null)
  // setReorderingAllowed(true) and the optional string argument for
  // addToBackStack() are both required if you want to use saveBackStack()
  .setReorderingAllowed(true)
  .addToBackStack("replacement")
  .commit();

在这种情况下,您可以通过调用saveBackStack()保存此片段事务以及ExampleFragment的状态

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

您可以使用相同的名称参数调用restoreBackStack()来恢复所有弹出的事务和所有保存的片段状态

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

向片段提供依赖项

添加片段时,您可以手动实例化片段并将其添加到FragmentTransaction

Kotlin

fragmentManager.commit {
    // Instantiate a new instance before adding
    val myFragment = ExampleFragment()
    add(R.id.fragment_view_container, myFragment)
    setReorderingAllowed(true)
}

Java

// Instantiate a new instance before adding
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
    .add(R.id.fragment_view_container, myFragment)
    .setReorderingAllowed(true)
    .commit();

提交片段事务时,您创建的片段实例就是使用的实例。但是,在配置更改期间,您的活动及其所有片段都将被销毁,然后使用最适用的Android 资源重新创建。FragmentManager为您处理所有这些:它重新创建片段的实例,将它们附加到主机,并重新创建回退栈状态。

默认情况下,FragmentManager使用框架提供的FragmentFactory来实例化片段的新实例。此默认工厂使用反射来查找并调用片段的无参数构造函数。这意味着您无法使用此默认工厂向片段提供依赖项。这也意味着您第一次创建片段时使用的任何自定义构造函数在默认情况下*不会*在重新创建期间使用。

要向您的片段提供依赖项,或使用任何自定义构造函数,请改为创建一个自定义的 FragmentFactory 子类,然后覆盖 FragmentFactory.instantiate。然后,您可以使用自定义工厂覆盖 FragmentManager 的默认工厂,该工厂随后用于实例化您的片段。

假设您有一个 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;
    }

    // Getter omitted.

    ...
}

您的 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 的子类,它覆盖了 instantiate() 方法,以提供 DessertsFragment 的自定义片段创建逻辑。其他片段类由 FragmentFactory 的默认行为通过 super.instantiate() 处理。

然后,您可以将 MyFragmentFactory 指定为在构建应用程序的片段时使用的工厂,方法是在 FragmentManager 上设置一个属性。您必须在 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 的片段层次结构中的片段创建。换句话说,您添加的任何子片段的 childFragmentManager 都使用此处设置的自定义片段工厂,除非在较低级别被覆盖。

使用 FragmentFactory 测试

在单 Activity 架构中,使用 FragmentScenario 类隔离测试您的片段。由于您无法依赖 Activity 的自定义 onCreate 逻辑,因此您可以改为将 FragmentFactory 作为参数传递给您的片段测试,如下例所示

// Inside your test
val dessertRepository = mock(DessertsRepository::class.java)
launchFragment<DessertsFragment>(factory = MyFragmentFactory(dessertRepository)).onFragment {
    // Test Fragment logic
}

有关此测试过程的详细信息以及完整示例,请参阅 测试您的片段