要重用片段,请将其构建为完全自包含的组件,这些组件定义自己的布局和行为。定义这些可重用片段后,您可以将其与活动关联并将其与应用程序逻辑连接起来,以实现整体复合 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 中,通过使用相同的requestKey
在相同的FragmentManager
上设置结果。您可以使用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. } }); } }