Komunikacja za pomocą fragmentów

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.
        }
    });
}
fragment b wysyła dane do fragmentu a za pomocą obiektu FragmentManager
Rysunek 1. Fragment B wysyła dane do fragmentu A za pomocą FragmentManager.

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 może użyć obiektu FragmentManager do wysłania wyniku do elementu nadrzędnego
Rysunek 2. Fragment podrzędny może użyć FragmentManager do wysłania wyniku do elementu nadrzędnego.

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.
            }
        });
    }
}