Aby ponownie używać fragmentów, utwórz je jako całkowicie niezależne komponenty, które definiują własny układ i zachowanie. Po zdefiniowaniu tych fragmentów wielokrotnego użytku możesz je powiązać z działaniem i połączyć z logiką aplikacji, aby uzyskać ogólny interfejs złożony.
Aby prawidłowo reagować na zdarzenia użytkownika i udostępniać informacje o stanie, często trzeba mieć kanały komunikacji między działaniem a jego fragmentami lub między co najmniej 2 fragmentami. Aby fragmenty były samodzielne, nie powinny one komunikować się bezpośrednio z innymi fragmentami ani z ich aktywnością hosta.
Biblioteka Fragment
udostępnia 2 opcje komunikacji: udostępnioną ViewModel
i interfejs FragmentResult API. Zalecana opcja zależy od konkretnego przypadku użycia. Aby udostępniać trwałe dane niestandardowych interfejsom API, użyj ViewModel
. W przypadku jednorazowego wyniku z danymi, które można umieścić w obiekcie Bundle
, użyj interfejsu FragmentResult API.
W sekcjach poniżej pokazujemy, jak używać interfejsu ViewModel
i interfejsu FragmentResult API do komunikacji między fragmentami a działaniami.
Udostępnianie danych za pomocą modelu ViewModel
ViewModel
to idealne rozwiązanie, gdy trzeba udostępniać dane między wieloma fragmentami lub między fragmentami a ich aktywnością hosta.
Obiekty ViewModel
przechowują dane UI i nimi zarządzają. Więcej informacji o metodzie ViewModel
znajdziesz w artykule Omówienie modelu ViewModel.
Udostępniaj dane aktywności hosta
W niektórych przypadkach może być konieczne udostępnianie danych między fragmentami i ich aktywnością hosta. Można np. przełączać globalny komponent UI na podstawie interakcji w obrębie danego fragmentu.
Weź pod uwagę te 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; } }
W tym przykładzie przechowywane dane są umieszczone w klasie MutableLiveData
.
LiveData
to klasa posiadacza danych, która uwzględnia cykl życia. MutableLiveData
pozwala zmienić jego wartość. Więcej informacji o LiveData
znajdziesz w omówieniu LiveData.
Zarówno Twój fragment, jak i jego aktywność hosta mogą pobierać udostępnioną instancję obiektu ViewModel
z zakresem aktywności przez przekazanie działania do konstruktora ViewModelProvider
. ViewModelProvider
tworzy instancję ViewModel
lub pobiera ją, jeśli już istnieje. Oba komponenty mogą obserwować i modyfikować
te dane.
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); }); } }
Udostępnianie danych między fragmentami
Co najmniej 2 fragmenty w tej samej aktywności muszą często komunikować się ze sobą. Weźmy na przykład jeden fragment, który zawiera listę, i drugi, który umożliwia użytkownikowi stosowanie do listy różnych filtrów. Wdrożenie w tym przypadku nie jest proste, jeśli fragmenty komunikują się bezpośrednio, ale nie są one już samodzielne. Dodatkowo oba fragmenty muszą obsługiwać scenariusz, w którym drugi fragment nie jest jeszcze utworzony ani widoczny.
Do obsługi tej komunikacji te fragmenty mogą udostępniać element ViewModel
za pomocą zakresu aktywności. Udostępniając w ten sposób parametr ViewModel
, fragmenty nie muszą się o sobie informować, a działanie nie musi ułatwiać komunikacji.
Ten przykład pokazuje, jak 2 fragmenty mogą używać udostępnionego elementu ViewModel
do komunikacji:
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); } }
Oba fragmenty wykorzystują aktywność hosta jako zakres obiektu ViewModelProvider
. Ponieważ fragmenty mają ten sam zakres, otrzymują tę samą instancję ViewModel
, co umożliwia im komunikację tam i z powrotem.
Udostępnianie danych między fragmentem nadrzędnym i podrzędnym
Podczas pracy z fragmentami podrzędnymi może być konieczne udostępnianie sobie nawzajem danych przez fragment nadrzędny i jego fragmenty podrzędne. Aby udostępniać dane między tymi fragmentami, użyj fragmentu nadrzędnego jako zakresu ViewModel
, jak pokazano w tym przykładzie:
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); ... } }
Określanie zakresu modelu widoku danych na wykresie nawigacyjnym
Jeśli korzystasz z biblioteki nawigacji, możesz też określić zakres ViewModel
na cykl życia elementu docelowego NavBackStackEntry
. Na przykład pole ViewModel
może być ograniczone do NavBackStackEntry
w przypadku ListFragment
:
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. } } }
Więcej informacji o ograniczaniu zakresu od ViewModel
do NavBackStackEntry
znajdziesz w artykule Automatyczna interakcja z komponentem Nawigacja.
Uzyskiwanie wyników za pomocą interfejsu Fragment Result API
W niektórych przypadkach warto przekazać jednorazową wartość między 2 fragmentami lub między fragmentem a jego aktywnością hosta. Na przykład możesz mieć fragment odczytujący kody QR i przekazujący dane z powrotem do poprzedniego fragmentu.
W komponencie Fragment w wersji 1.3.0 lub nowszej każdy FragmentManager
implementuje FragmentResultOwner
.
Oznacza to, że obiekt FragmentManager
może działać jako centralna baza wyników opartych na fragmentach. Ta zmiana umożliwia komponentom komunikowanie się ze sobą przez ustawianie wyników fragmentów i nasłuchiwanie tych wyników, bez konieczności bezpośredniego odwołania do siebie tych komponentów.
Przekazywanie wyników między fragmentami
Aby przekazać dane z powrotem do fragmentu A z fragmentu B, najpierw ustaw odbiornik wyników dla fragmentu A, czyli fragmentu, który odbiera wynik. Wywołanie setFragmentResultListener()
we fragmencie A FragmentManager
, jak w tym przykładzie:
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. } }); }
We fragmencie B, fragment będący wynikiem, ustaw wynik na tym samym elemencie FragmentManager
przy użyciu tego samego parametru requestKey
. Możesz to zrobić za pomocą interfejsu API setFragmentResult()
:
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); } });
Następnie fragment A otrzymuje wynik i wykona wywołanie zwrotne detektora, gdy fragment będzie miał wartość STARTED
.
Możesz mieć tylko 1 detektor i wynik dla danego klawisza. Jeśli wywołasz właściwość setFragmentResult()
więcej niż raz dla tego samego klucza, a odbiornik nie to STARTED
, system zastąpi wszystkie oczekujące wyniki zaktualizowanym wynikiem.
Jeśli ustawisz odbiornik, który nie ma odbiornika, wynik będzie przechowywany w FragmentManager
do momentu ustawienia odbiornika z tym samym klawiszem. Gdy odbiornik otrzyma wynik i uruchomi wywołanie zwrotne onFragmentResult()
, wynik zostanie wyczyszczony. To zachowanie ma 2 główne konsekwencje:
- Fragmenty w stosie wstecznym nie otrzymują wyników, dopóki nie zostaną pobrane i nie będą miały stanu
STARTED
. - Jeśli fragment nasłuchujący wyniku ma wartość
STARTED
w momencie ustawienia wyniku, wywołanie zwrotne detektora uruchamia się natychmiast.
Wyniki testu fragmentu
Użyj FragmentScenario
, aby przetestować wywołania setFragmentResult()
i setFragmentResultListener()
.
Utwórz scenariusz dla testowanego fragmentu za pomocą funkcji launchFragmentInContainer
lub launchFragment
, a potem ręcznie wywołaj metodę, która nie jest testowana.
Aby przetestować setFragmentResultListener()
, utwórz scenariusz z fragmentem powodującym wywołanie setFragmentResultListener()
. Następnie zadzwoń bezpośrednio do firmy setFragmentResult()
i sprawdź wynik:
@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")
}
}
}
Aby przetestować setFragmentResult()
, utwórz scenariusz z fragmentem wywołującym wywołanie setFragmentResult()
. Następnie zadzwoń bezpośrednio do usługi setFragmentResultListener()
i sprawdź wynik:
@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))
}
}
}
Przekazywanie wyników między fragmentami nadrzędnymi i podrzędnymi
Aby przekazać wynik z fragmentu podrzędnego do elementu nadrzędnego, podczas wywoływania metody setFragmentResultListener()
użyj polecenia getChildFragmentManager()
z fragmentu nadrzędnego zamiast 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. } }); }
Fragment podrzędny ustawia wynik na elemencie FragmentManager
. Element nadrzędny otrzymuje wynik, gdy fragment ma wartość 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); } });
Otrzymuj wyniki w aktywności hosta
Aby otrzymać wynik dotyczący fragmentu w aktywności hosta, ustaw detektor wyników w menedżerze fragmentów za pomocą metody 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. } }); } }