如要重複使用片段,請將片段建構為完全獨立的元件,讓片段自行定義版面配置和行為。定義可重複使用的片段後,您可以將這些片段與活動建立關聯,並與應用程式邏輯建立連結,形成複合的整體 UI。
如要正確回應使用者事件或分享狀態資訊,您通常需要在活動和活動片段之間,或是兩個以上的片段之間具備通訊管道。為了讓片段保持獨立,「請勿」讓片段直接與其他片段或其主機活動通訊。
Fragment
程式庫提供兩種通訊選項:共用的 ViewModel
,以及 Fragment Result API。建議做法視用途而異。若要與自訂 API 共用持續性資料,請使用 ViewModel
。若是一次性結果,且其中的資料可放置在 Bundle
,請使用 Fragment Result API。
以下章節介紹如何使用 ViewModel
和 Fragment Result 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 的範圍限定為導覽圖
如果使用 Navigation 程式庫,也可以將 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
,請參閱「透過程式輔助方法與 Navigation 元件互動」。
使用 Fragment Result API 取得結果
在某些情況下,您可能會想在兩個片段之間,或是在片段和其主機活動之間傳遞一次性的值。舉例來說,您可能有一個片段可讀取 QR code,並將資料傳回至前一個片段。
若是 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. } }); } }