为了重复使用 Fragment,请将每个 Fragment 构建为一个完全独立的组件,使其定义自己的布局和行为。定义这些可重复使用的 Fragment 后,您可以将其与 Activity 关联,并将其与应用逻辑联系起来,以实现整体复合界面。
为了正确响应用户事件,或为了共享状态信息,通常需要在 Activity 与其 Fragment 之间或者两个或更多 Fragment 之间具有通信渠道。为使 Fragment 保持独立,您不应让 Fragment 直接与其他 Fragment 或与其宿主 Activity 进行通信。
Fragment
库提供了两个通信选项:共享的 ViewModel 和 Fragment Result API。建议的选项取决于用例。如需与任何自定义 API 共享持久性数据,您应使用 ViewModel
。对于包含的数据可放置在 Bundle 中的一次性结果,您应使用 Fragment Result API。
下面几部分为您介绍了如何使用 ViewModel
和 Fragment Result API 在 Fragment 与 Activity 之间进行通信。
使用 ViewModel
共享数据
当您需要在多个 Fragment 之间或 Fragment 与其宿主 Activity 之间共享数据时,ViewModel
是理想的选择。ViewModel 对象可存储和管理界面数据。如需详细了解 ViewModel
,请参阅 ViewModel 概览。
与宿主 Activity 共享数据
在某些情况下,您可能需要在 Fragment 与其宿主 Activity 之间共享数据。例如,您可能想要根据 Fragment 中的交互切换一个全局界面组件。
我们来考虑以下 ItemViewModel
:
public class ItemViewModel extends ViewModel { private final MutableLiveData<Item> selectedItem = new MutableLiveData<Item>(); public void selectItem(Item item) { selectedItem.setValue(item); } public LiveData<Item> getSelectedItem() { return selectedItem; } }
在本例中,当前存储的数据封装在 MutableLiveData 类中。LiveData 是生命周期感知型可观察数据存储器类。MutableLiveData
允许更改其值。如需详细了解 LiveData
,请参阅 LiveData 概览。
Fragment 及其宿主 Activity 均可通过将 Activity 传入 ViewModelProvider 构造函数来使用 Activity 范围检索 ViewModel
的共享实例。ViewModelProvider
负责实例化 ViewModel
或检索它(如果已存在)。这两个组件都可以观察和修改此数据:
public class MainActivity extends AppCompatActivity { private ItemViewModel viewModel; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); viewModel = new ViewModelProvider(this).get(ItemViewModel.class); viewModel.getSelectedItem().observe(this, item -> { // Perform an action with the latest item data }); } } public class ListFragment extends Fragment { private ItemViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(requireActivity()).get(ItemViewModel.class); ... items.setOnClickListener(item -> { // Set a new item viewModel.select(item); }); } }
注意:务必将适当的范围与 ViewModelProvider
一起使用。在前面的示例中,MainActivity
用作 MainActivity
和 ListFragment
中的范围,因此为它们提供了相同的 ViewModel
。如果 ListFragment
将自身用作范围,会为其提供与 MainActivity
不同的 ViewModel
。
在 Fragment 之间共享数据
同一 Activity 中的两个或更多 Fragment 通常需要相互通信。例如,假设有一个 Fragment 显示一个列表,另一个 Fragment 允许用户对该列表应用各种过滤器。如果 Fragment 不直接通信(这意味着,它们不再独立),这种情况可能不容易实现。此外,这两个 Fragment 还必须处理另一个 Fragment 尚未创建或不可见的情况。
这两个 Fragment 可以使用其 Activity 范围共享 ViewModel
来处理这种通信。通过以这种方式共享 ViewModel
,Fragment 不需要相互了解,Activity 也不需要执行任何操作来促进通信。
以下示例展示了两个 Fragment 如何使用共享的 ViewModel
进行通信:
public class ListViewModel extends ViewModel { private final MutableLiveData<Set<Filter>> filters = new MutableLiveData<>(); private final LiveData<List<Item>> originalList = ...; private final LiveData<List<Item>> filteredList = ...; public LiveData<List<Item>> getFilteredList() { return filteredList; } public LiveData<Set<Filter>> getFilters() { return filters; } public void addFilter(Filter filter) { ... } public void removeFilter(Filter filter) { ... } } public class ListFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class); viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> { // Update the list UI }); } } public class FilterFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { viewModel = new ViewModelProvider(requireActivity()).get(ListViewModel.class); viewModel.getFilters().observe(getViewLifecycleOwner(), set -> { // Update the selected filters UI }); } public void onFilterSelected(Filter filter) { viewModel.addFilter(filter); } public void onFilterDeselected(Filter filter) { viewModel.removeFilter(filter); } }
请注意,这两个 Fragment 都将其宿主 Activity 用作 ViewModelProvider
的范围。由于这两个 Fragment 使用同一范围,因此它们会收到 ViewModel
的同一实例,这使它们能够来回通信。
注意:ViewModel
会一直在内存中,直到其范围限定到的 ViewModelStoreOwner 永久消失。在一个 Activity 架构中,如果 ViewModel
的范围限定为 Activity,那么它本质上是单例。首次实例化 ViewModel
之后,使用 Activity 范围检索 ViewModel
的后续调用始终返回相同的现有 ViewModel
以及现有数据,直到 Activity 的生命周期永久结束。
在父 Fragment 与子 Fragment 之间共享数据
使用子 Fragment 时,父 Fragment 及其子 Fragment 可能需要相互共享数据。如需在这些 Fragment 之间共享数据,请将父 Fragment 用作 ViewModel
范围。
public class ListFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { viewModel = new ViewModelProvider(this).get(ListViewModel.class); viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> { // Update the list UI } } } public class ChildFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { viewModel = new ViewModelProvider(requireParentFragment()).get(ListViewModel.class); ... } }
将 ViewModel
的范围限定为导航图
如果您使用的是 Navigation 库,还可以将 ViewModel
的范围限定为目的地的 NavBackStackEntry 的生命周期。例如,可以将 ViewModel
的范围限定为 ListFragment
的 NavBackStackEntry
:
public class ListFragment extends Fragment { private ListViewModel viewModel; @Override public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { NavController navController = NavHostFragment.findNavController(this); NavBackStackEntry backStackEntry = navController.getBackStackEntry(R.id.list_fragment) viewModel = new ViewModelProvider(backStackEntry).get(ListViewModel.class); viewModel.getFilteredList().observe(getViewLifecycleOwner(), list -> { // Update the list UI } } }
如需详细了解如何将 ViewModel
的范围限定为 NavBackStackEntry
,请参阅以编程方式与 Navigation 组件交互。
使用 Fragment Result API 获取结果
在某些情况下,您可能要在 Fragment 之间或 Fragment 与其宿主 Activity 之间传递一次性值。例如,您可能有一个 Fragment,它读取二维码,并将数据传回之前的 Fragment。从 Fragment 1.3.0-alpha04 开始,每个 FragmentManager 都会实现 FragmentResultOwner。这意味着,FragmentManager
可以充当 Fragment 结果的集中存储区。此更改通过设置 Fragment 结果并监听这些结果而不要求组件直接相互引用,让这些组件能够相互通信。
在 Fragment 之间传递结果
如需将数据从 Fragment B 传回 Fragment A,请先在 Fragment A(即接收结果的 Fragment)上设置结果监听器。对 Fragment A 的 FragmentManager
调用 setFragmentResultListener(),如以下示例所示:
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getParentFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() { @Override public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) { // We use a String here, but any type that can be put in a Bundle is supported String result = bundle.getString("bundleKey"); // Do something with the result } }); }
图 1. Fragment B 使用 FragmentManager
将数据发送到 Fragment A。
在 Fragment B(即生成结果的 Fragment)中,您必须使用相同的 requestKey
在同一 FragmentManager
上设置结果。您可以使用 setFragmentResult() API 来完成此操作:
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle result = new Bundle(); result.putString("bundleKey", "result"); getParentFragmentManager().setFragmentResult("requestKey", result); } });
然后,一旦 Fragment A 处于 STARTED 状态,它就会收到结果并执行监听器回调。
对于给定的键,只能有一个监听器和结果。如果您对同一个键多次调用 setFragmentResult()
,并且监听器未处于 STARTED
状态,则系统会将所有待处理的结果替换为更新后的结果。如果您设置的结果没有相应的监听器来接收,则结果会存储在 FragmentManager
中,直到您设置一个具有相同键的监听器。监听器收到结果并触发 onFragmentResult()
回调后,结果会被清除。这种行为有两个主要影响:
- 返回堆栈上的 Fragment 只有在被弹出且处于
STARTED
状态之后才会收到结果。 - 如果在设置结果时监听结果的 Fragment 处于
STARTED
状态,则会立即触发监听器的回调。
注意:由于 Fragment 结果存储在 FragmentManager
级别,因此 Fragment 必须随父 FragmentManager
一起附加到对 setFragmentResultListener()
或 setFragmentResult()
的调用。
测试 Fragment 结果
使用 FragmentScenario 测试对 setFragmentResult()
和 setFragmentResultListener()
的调用。使用 launchFragmentInContainer 或 launchFragment 为被测 Fragment 创建一个场景,然后手动调用当前未测试的方法。
如需测试 setFragmentResultListener()
,请使用调用 setFragmentResultListener()
的 Fragment 创建一个场景。接下来,直接调用 setFragmentResult()
并验证结果:
@Test
fun testFragmentResultListener() {
val scenario = launchFragmentInContainer<ResultListenerFragment>()
scenario.onFragment { fragment ->
val expectedResult = "result"
fragment.parentFragmentManager.setFragmentResult("requestKey", bundleOf("bundleKey" to expectedResult))
assertThat(fragment.result).isEqualTo(expectedResult)
}
}
class ResultListenerFragment : Fragment() {
var result : String? = null
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResultListener("requestKey") { requestKey, bundle ->
result = bundle.getString("bundleKey")
}
}
}
如需测试 setFragmentResult()
,请使用调用 setFragmentResult()
的 Fragment 创建一个场景。接下来,直接调用 setFragmentResultListener()
并验证结果:
@Test
fun testFragmentResult() {
val scenario = launchFragmentInContainer<ResultFragment>()
lateinit var actualResult: String?
scenario.onFragment { fragment ->
fragment.parentFragmentManager
.setFragmentResultListener("requestKey") { requestKey, bundle ->
actualResult = bundle.getString("bundleKey")
}
}
onView(withId(R.id.result_button)).perform(click())
assertThat(actualResult).isEqualTo("result")
}
class ResultFragment : Fragment(R.layout.fragment_result) {
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
view.findViewById(R.id.result_button).setOnClickListener {
val result = "result"
// Use the Kotlin extension in the fragment-ktx artifact
setFragmentResult("requestKey", bundleOf("bundleKey" to result))
}
}
}
在父 Fragment 与子 Fragment 之间传递结果
如需将结果从子 Fragment 传递到父 Fragment,父 Fragment 在调用 setFragmentResultListener()
时应使用 getChildFragmentManager()
而不是 getParentFragmentManager()
。
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // We set the listener on the child fragmentManager getChildFragmentManager() .setFragmentResultListener("requestKey", this, new FragmentResultListener() { @Override public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) { String result = bundle.getString("bundleKey"); // Do something with the result } }); }
图 2 子 Fragment 可以使用 FragmentManager
将结果发送到其父 Fragment。
子 Fragment 在其 FragmentManager
上设置结果。然后,一旦父 Fragment 处于 STARTED
状态,它就会收到结果:
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle result = new Bundle(); result.putString("bundleKey", "result"); // The child fragment needs to still set the result on its parent fragment manager getParentFragmentManager().setFragmentResult("requestKey", result); } });
在宿主 Activity 中接收结果
如需在宿主 Activity 中接收 Fragment 结果,请使用 getSupportFragmentManager() 在 Fragment 管理器上设置结果监听器。
class MainActivity extends AppCompatActivity { @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); getSupportFragmentManager().setFragmentResultListener("requestKey", this, new FragmentResultListener() { @Override public void onFragmentResult(@NonNull String requestKey, @NonNull Bundle bundle) { // We use a String here, but any type that can be put in a Bundle is supported String result = bundle.getString("bundleKey"); // Do something with the result } }); } }