要重复使用片段,请将它们构建为完全独立的组件,这些组件定义自己的布局和行为。定义好这些可重复使用的片段后,您可以将它们与活动关联,并将其与应用程序逻辑连接起来,以实现整体的组合 UI。
为了适当地响应用户事件并共享状态信息,您通常需要在活动与其片段之间,或两个或多个片段之间建立通信渠道。为了保持片段的独立性,不要让片段直接与其他片段或其宿主活动进行通信。
Fragment
库提供了两种通信选项:共享的 ViewModel
和片段结果 API。推荐使用哪种选项取决于用例。若要使用自定义 API 共享持久数据,请使用 ViewModel
。对于可以放置在 Bundle
中的一次性结果数据,请使用片段结果 API。
以下部分将向您展示如何使用 ViewModel
和片段结果 API 在片段和活动之间进行通信。
使用 ViewModel 共享数据
ViewModel
是您需要在多个片段之间或在片段与其宿主活动之间共享数据时的理想选择。 ViewModel
对象存储和管理 UI 数据。有关 ViewModel
的更多信息,请参阅 ViewModel 概述。
与宿主活动共享数据
在某些情况下,您可能需要在片段与其宿主活动之间共享数据。例如,您可能希望根据片段内的交互来切换全局 UI 组件。
考虑以下 ItemViewModel
Kotlin
class ItemViewModel : ViewModel() { private val mutableSelectedItem = MutableLiveData<Item>() val selectedItem: LiveData<Item> get() = mutableSelectedItem fun selectItem(item: Item) { mutableSelectedItem.value = item } }
Java
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 概述。
您的片段及其宿主活动都可以通过将活动传递到 ViewModelProvider
构造函数中来检索具有活动范围的 ViewModel
的共享实例。 ViewModelProvider
处理实例化 ViewModel
或检索它(如果它已经存在)。这两个组件都可以观察和修改此数据。
Kotlin
class MainActivity : AppCompatActivity() { // Using the viewModels() Kotlin property delegate from the activity-ktx // artifact to retrieve the ViewModel in the activity scope. private val viewModel: ItemViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel.selectedItem.observe(this, Observer { item -> // Perform an action with the latest item data. }) } } class ListFragment : Fragment() { // Using the activityViewModels() Kotlin property delegate from the // fragment-ktx artifact to retrieve the ViewModel in the activity scope. private val viewModel: ItemViewModel by activityViewModels() // Called when the item is clicked. fun onItemClicked(item: Item) { // Set a new item. viewModel.selectItem(item) } }
Java
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); }); } }
在片段之间共享数据
同一活动中的两个或多个片段通常需要彼此通信。例如,想象一个显示列表的片段和另一个允许用户对列表应用各种过滤器的片段。如果没有片段直接通信,实现这种情况并不容易,但那样一来,它们就不再是自包含的了。此外,两个片段都必须处理另一个片段尚未创建或可见的情况。
这些片段可以使用它们活动的作用域共享一个 ViewModel
来处理这种通信。通过这种方式共享 ViewModel
,片段不需要知道彼此的存在,活动也不需要做任何事情来促进通信。
以下示例展示了两个片段如何使用共享的 ViewModel
进行通信
Kotlin
class ListViewModel : ViewModel() { val filters = MutableLiveData<Set<Filter>>() private val originalList: LiveData<List<Item>>() = ... val filteredList: LiveData<List<Item>> = ... fun addFilter(filter: Filter) { ... } fun removeFilter(filter: Filter) { ... } } class ListFragment : Fragment() { // Using the activityViewModels() Kotlin property delegate from the // fragment-ktx artifact to retrieve the ViewModel in the activity scope. private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { list -> // Update the list UI. } } } class FilterFragment : Fragment() { private val viewModel: ListViewModel by activityViewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filters.observe(viewLifecycleOwner, Observer { set -> // Update the selected filters UI. } } fun onFilterSelected(filter: Filter) = viewModel.addFilter(filter) fun onFilterDeselected(filter: Filter) = viewModel.removeFilter(filter) }
Java
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); } }
这两个片段都使用它们的宿主活动作为 ViewModelProvider
的作用域。由于片段使用相同的作用域,它们接收到的 ViewModel
实例相同,从而使它们能够相互通信。
在父片段和子片段之间共享数据
在处理子片段时,父片段及其子片段可能需要彼此共享数据。要在这两个片段之间共享数据,请使用父片段作为 ViewModel
作用域,如以下示例所示
Kotlin
class ListFragment: Fragment() { // Using the viewModels() Kotlin property delegate from the fragment-ktx // artifact to retrieve the ViewModel. private val viewModel: ListViewModel by viewModels() override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { list -> // Update the list UI. } } } class ChildFragment: Fragment() { // Using the viewModels() Kotlin property delegate from the fragment-ktx // artifact to retrieve the ViewModel using the parent fragment's scope private val viewModel: ListViewModel by viewModels({requireParentFragment()}) ... }
Java
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 作用域到导航图
如果您正在使用 导航库,您还可以将 ViewModel
作用域到目的地的 NavBackStackEntry
的生命周期。例如,可以将 ViewModel
作用域到 ListFragment
的 NavBackStackEntry
Kotlin
class ListFragment: Fragment() { // Using the navGraphViewModels() Kotlin property delegate from the fragment-ktx // artifact to retrieve the ViewModel using the NavBackStackEntry scope. // R.id.list_fragment == the destination id of the ListFragment destination private val viewModel: ListViewModel by navGraphViewModels(R.id.list_fragment) override fun onViewCreated(view: View, savedInstanceState: Bundle?) { viewModel.filteredList.observe(viewLifecycleOwner, Observer { item -> // Update the list UI. } } }
Java
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
的更多信息,请参阅 以编程方式与导航组件交互。
使用片段结果 API 获取结果
在某些情况下,您可能希望在两个片段之间或在片段与其宿主活动之间传递一次性值。例如,您可能有一个读取二维码的片段,并将数据传回前一个片段。
在 Fragment 版本 1.3.0 及更高版本中,每个 FragmentManager
都实现了 FragmentResultOwner
。这意味着 FragmentManager
可以充当片段结果的中央存储库。这种改变允许组件通过设置片段结果并监听这些结果来相互通信,而无需这些组件彼此具有直接引用。
在片段之间传递结果
要将数据从片段 B 传回片段 A,首先在片段 A(接收结果的片段)上设置结果监听器。在片段 A 的 FragmentManager
上调用 setFragmentResultListener()
,如以下示例所示
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Use the Kotlin extension in the fragment-ktx artifact. setFragmentResultListener("requestKey") { requestKey, bundle -> // We use a String here, but any type that can be put in a Bundle is supported. val result = bundle.getString("bundleKey") // Do something with the result. } }
Java
@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. } }); }
在片段 B(生成结果的片段)中,在相同的 FragmentManager
上设置结果,方法是使用相同的 requestKey
。您可以使用 setFragmentResult()
API 来完成此操作
Kotlin
button.setOnClickListener { val result = "result" // Use the Kotlin extension in the fragment-ktx artifact. setFragmentResult("requestKey", bundleOf("bundleKey" to result)) }
Java
button.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { Bundle result = new Bundle(); result.putString("bundleKey", "result"); getParentFragmentManager().setFragmentResult("requestKey", result); } });
然后,片段 A 接收结果,并在片段处于 STARTED
状态后执行监听器回调。
对于给定的键,您只能拥有一个监听器和结果。如果您多次为同一个键调用 setFragmentResult()
,并且监听器没有处于 STARTED
状态,系统会将任何挂起的結果替换为您的更新结果。
如果您在没有相应的监听器来接收结果的情况下设置了结果,则该结果将存储在 FragmentManager
中,直到您使用相同的键设置监听器为止。一旦监听器接收到结果并触发 onFragmentResult()
回调,结果将被清除。这种行为有两个主要含义
- 回退栈中的片段只有在被弹出并处于
STARTED
状态后才会接收到结果。 - 如果监听结果的片段在设置结果时处于
STARTED
状态,那么监听器的回调将立即触发。
测试片段结果
使用 FragmentScenario
来测试对 setFragmentResult()
和 setFragmentResultListener()
的调用。使用 launchFragmentInContainer
或 launchFragment
为正在测试的片段创建场景,然后手动调用未被测试的方法。
要测试 setFragmentResultListener()
,请使用调用 setFragmentResultListener()
的片段创建场景。接下来,直接调用 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()
的片段创建场景。接下来,直接调用 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))
}
}
}
在父片段和子片段之间传递结果
要将结果从子片段传递到父片段,请在调用 setFragmentResultListener()
时,从父片段使用 getChildFragmentManager()
而不是 getParentFragmentManager()
。
Kotlin
override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Set the listener on the child fragmentManager. childFragmentManager.setFragmentResultListener("requestKey") { key, bundle -> val result = bundle.getString("bundleKey") // Do something with the result. } }
Java
@Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); // 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. } }); }
子片段在其 FragmentManager
上设置结果。父片段在片段处于 STARTED
状态后接收结果
Kotlin
button.setOnClickListener { val result = "result" // Use the Kotlin extension in the fragment-ktx artifact. setFragmentResult("requestKey", bundleOf("bundleKey" to result)) }
Java
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); } });
在宿主活动中接收结果
要在宿主活动中接收片段结果,请使用 getSupportFragmentManager()
在片段管理器上设置结果监听器。
Kotlin
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) supportFragmentManager .setFragmentResultListener("requestKey", this) { requestKey, bundle -> // We use a String here, but any type that can be put in a Bundle is supported. val result = bundle.getString("bundleKey") // Do something with the result. } } }
Java
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. } }); } }