Android开发--FragmentManager

概要

FragmentManager 类负责在应用的 fragment 上执行一些操作,
如添加、移除或替换操作,以及将操作添加到返回堆栈

访问 FragmentManager

我们可以在Activity和Fragment中访问FragmentManager,下面依次介绍两种访问方式和联系
1.在 Activity 中访问
每个 FragmentActivity 及其子类(如 AppCompatActivity)
都可以通过 getSupportFragmentManager() 方法访问 FragmentManager
2.在 Fragment 中访问
Fragment 也能够托管一个或多个子 fragment。在 fragment 内,
您可以通过 getChildFragmentManager() 获取对管理 fragment 子级的 FragmentManager 的引用。
如果您需要访问其宿主 FragmentManager,可以使用 getParentFragmentManager()

托管 Fragment

fragment、其宿主以及与每个 fragment 关联的 FragmentManager 实例之间的关系如下图所示
两个界面布局示例,显示了 fragment 与其宿主 activity 之间的关系。每个示例中都有一个 activity 宿主。
这两个示例中的宿主 activity 都以 BottomNavigationView 的形式向用户显示顶级导航,
该视图负责以应用中的不同屏幕换出宿主 fragment,其中每个屏幕都实现为单独的 fragment

示例 1 中的宿主 fragment 托管两个子 fragment,这些子 fragment 构成拆分视图屏幕。
示例 2 中的宿主 fragment 托管一个子 fragment,该子 fragment 构成滑动视图的显示 fragment。

基于此设置,您可以将每个宿主视为具有与其关联的 FragmentManager,用于管理其子 fragment。
下图说明了这一点,并显示了 supportFragmentManager、parentFragmentManager 和 childFragmentManager 之间的属性映射
每个宿主都有与其关联的 FragmentManager,用于管理其子 fragment。
需要引用的相应 FragmentManager 属性取决于调用点在 fragment 层次结构中的位置,
以及您尝试访问的 fragment 管理器。对 FragmentManager 进行引用后,您就可以使用它来操纵向用户显示的 fragment。

子 fragment 的管理

一般来说,应用应由应用项目中的一个或少数几个 activity 组成,其中每个 activity 表示一组相关的屏幕。
activity 可能会提供一个点来放置顶级导航,并提供一个位置来限定 ViewModels 以及 fragment 之间的其他视图状态的范围。
应用中的每个目的地应由一个 fragment 来表示。

如果您想要一次显示多个 fragment(如在拆分视图中或信息中心内),应使用子 fragment,
它们由目的地 fragment 及其子 fragment 管理器进行管理。子 fragment 的其他用例可能包括:

1.屏幕滑动,其中父 fragment 中的 ViewPager2 管理一系列子 fragment 视图。
2.一组相关屏幕中的子导航。
3.Jetpack Navigation 将子 fragment 用作各个目的地。一个 activity 托管一个父 NavHostFragment,
    并在用户浏览应用时以不同的子目的地 fragment 填充它的空间。

执行事务

如需在布局容器中显示 fragment,请使用 FragmentManager 创建 FragmentTransaction。
在事务中,您随后可以对容器执行 add() 或 replace() 操作。

例如,一个简单的 FragmentTransaction 可能如下所示:
1
2
3
4
5
6
FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
.replace(R.id.fragment_container, ExampleFragment.class, null)
.setReorderingAllowed(true)
.addToBackStack("name") // name can be null
.commit();
在本例中,ExampleFragment 会替换当前在布局容器中的 fragment(如有),
该布局容器由 R.id.fragment_container ID 进行标识。
将 fragment 的类提供给 replace() 方法可让 FragmentManager 使用其 FragmentFactory 处理实例化

setReorderingAllowed(true) 可优化事务中涉及的 fragment 的状态变化,
以使动画和过渡正常运行。如需详细了解如何使用动画和过渡进行导航

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

如果您在执行移除 fragment 的事务时未调用 addToBackStack(),则提交事务时会销毁已移除的 fragment,
用户无法返回到该 fragment。如果您在移除某个 fragment 时调用了 addToBackStack(),
则该 fragment 只会 STOPPED,稍后当用户返回时它会 RESUMED。请注意,在这种情况下,其视图会被销毁

查找现有 fragment

可以通过 findFragmentById() 或者 findFragmentByTag() 获取对布局容器中当前 fragment 的引用
findFragmentById()
可使用 findFragmentById() 按给定的 ID 查找 fragment;
在 FragmentTransaction 中添加时,可使用它按容器 ID 进行查找
1
2
3
4
5
6
7
8
9
10
11
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()
可以为 fragment 分配一个唯一的标记,并使用 findFragmentByTag() 获取引用。
您可以在布局中定义的 fragment 上使用 android:tag XML 属性来分配标记,
也可以在 FragmentTransaction 中的 add() 或 replace() 操作期间分配标记
1
2
3
4
5
6
7
8
9
10
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 的特殊注意事项

1.在任何给定的时间点,只允许一个 FragmentManager 控制 fragment 返回堆栈。

2.如果应用在屏幕上同时显示多个同级 fragment,或者应用使用子 fragment,
  则必须指定一个 FragmentManager 来处理应用的主要导航。  

3.如需在 fragment 事务内定义主要导航 fragment,请对事务调用 setPrimaryNavigationFragment() 方法,
  并传入一个 fragment 的实例,该 fragment 的 childFragmentManager 应具有主要控制权。

4.将导航结构视为一系列层,其中 activity 作为最外层,封装下面的每一层子 fragment。
  每一层都必须有一个主要导航 fragment。当发生返回事件时,最内层控制导航行为。
  一旦最内层再也没有可从其弹回的 fragment 事务,控制权就会向外回一层,此过程会一直重复,直至到达 activity 为止。

5.当同时显示两个或更多 fragment 时,其中只有一个可以是主要导航 fragment。

6.如果将某个 fragment 设为主要导航 fragment,会移除对先前 fragment 的指定。在上例中,
  如果您将详情 fragment 设为主要导航 fragment,就会移除对主 fragment 的指定。

支持多个返回堆栈

在某些情况下,您的应用可能需要支持多个返回堆栈。一个常见示例是,您的应用使用底部导航栏。
FragmentManager 可让您通过 saveBackStack() 和 restoreBackStack() 方法支持多个返回堆栈。
这两种方法使您可以通过保存一个返回堆栈并恢复另一个返回堆栈来在返回堆栈之间进行交换。

saveBackStack() 的工作方式类似于使用可选 name 参数调用 popBackStack():
弹出指定事务以及堆栈上在此之后的所有事务。不同之处在于 saveBackStack() 
会保存弹出事务中所有 fragment 的状态。

例如,假设您之前使用 addToBackStack() 提交 FragmentTransaction,从而将 fragment 添加到返回堆栈
1
2
3
4
5
6
7
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();
在这种情况下,您可以通过调用 saveState() 来保存此 fragment 事务和 ExampleFragment 的状态:
1
supportFragmentManager.saveBackStack("replacement");
注意:您只能将 saveBackStack() 用于调用 setReorderingAllowed(true) 的事务,
         以确保可以将事务还原为单一原子操作
您可以使用相同的名称参数调用 restoreBackStack(),以恢复所有弹出的事务以及所有保存的 fragment 状态
1
supportFragmentManager.restoreBackStack("replacement");
注意:除非使用 addToBackStack() 传递 fragment 事务的可选名称,
否则不能使用 saveBackStack() 和 restoreBackStack()

为 fragment 提供依赖项

添加 fragment 时,您可以手动实例化 fragment 并将其添加到 FragmentTransaction
1
2
3
4
5
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
.add(R.id.fragment_view_container, myFragment)
.setReorderingAllowed(true)
.commit();
当您提交 fragment 事务时,您创建的 fragment 实例就是使用的实例。不过,在配置更改期间,
activity 及其所有 fragment 都会被销毁,然后使用最适用的 Android 资源重新创建。
FragmentManager 会为您处理所有这些操作。
它会重新创建 fragment 的实例,将其附加到宿主,并重新创建返回堆栈状态。

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

如需为 fragment 提供依赖项或使用任何自定义构造函数,
您必须创建自定义 FragmentFactory 子类,然后替换 FragmentFactory.instantiate。
您随后可以将 FragmentManager 的默认工厂替换为您的自定义工厂,该自定义工厂随后将用于实例化 fragment

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

您可以将 DessertsFragment 定义为在其构造函数中需要 DessertsRepository 实例
1
2
3
4
5
6
7
8
9
10
11
12
public class DessertsFragment extends Fragment {
private DessertsRepository dessertsRepository;

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

// Getter omitted.

...
}
FragmentFactory 的简单实现可能与以下代码类似
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 提供自定义 fragment 创建逻辑。
其他 fragment 类通过 super.instantiate() 由 FragmentFactory 的默认行为进行处理。

您随后可以通过在 FragmentManager 上设置一个属性,
将 MyFragmentFactory 指定为要在构造应用的 fragment 时使用的工厂。
您必须在 activity 的 super.onCreate() 之前设置此属性,
以确保在重新创建 fragment 时使用 MyFragmentFactory
1
2
3
4
5
6
7
8
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 工厂,除非在较低的级别被替换