Menedżer fragmentów

FragmentManager to klasa odpowiedzialna za wykonywanie działań na fragmentach aplikacji, takich jak dodawanie, usuwanie lub zastępowanie ich oraz dodawanie ich do stosu wstecznego.

Jeśli używasz biblioteki Jetpack Navigation, możesz nigdy nie korzystać z usługi FragmentManager bezpośrednio, ponieważ działa ona w Twoim imieniu z usługą FragmentManager. Każda aplikacja używająca fragmentów korzysta jednak na pewnym poziomie z elementu FragmentManager, dlatego warto wiedzieć, co to jest i jak działa.

Na tej stronie znajdziesz te informacje:

  • Jak otworzyć FragmentManager.
  • Rola FragmentManager w odniesieniu do Twoich działań i fragmentów.
  • Jak zarządzać stosem wstecznym w FragmentManager.
  • sposób udostępniania danych i zależności we fragmentach;

Dostęp do FragmentManagera

Dostęp do funkcji FragmentManager możesz uzyskać z poziomu aktywności lub z poziomu fragmentu.

FragmentActivity i jego podklasy, takie jak AppCompatActivity, mają dostęp do klasy FragmentManager za pomocą metody getSupportFragmentManager().

Fragmenty mogą zawierać jeden lub więcej fragmentów podrzędnych. We fragmencie możesz uzyskać odniesienie do elementu FragmentManager, który zarządza elementami podrzędnymi danego fragmentu za pomocą getChildFragmentManager(). Jeśli chcesz uzyskać dostęp do hosta FragmentManager, możesz użyć getParentFragmentManager().

Oto 2 przykłady pokazujące relacje między fragmentami, ich hostami i powiązanymi z nimi instancjami FragmentManager.

2 przykłady układu interfejsu pokazujące relacje między fragmentami i ich działaniami hosta
Rysunek 1. Dwa przykłady układu interfejsu pokazujące relacje między fragmentami i ich działaniami hosta.

Rysunek 1 pokazuje 2 przykłady, z których każdy zawiera jednego hosta aktywności. Aktywność hosta w obu tych przykładach powoduje wyświetlenie użytkownikowi nawigacji najwyższego poziomu w postaci elementu BottomNavigationView, który odpowiada za zamianę fragmentu hosta na inne ekrany w aplikacji. Każdy ekran jest zaimplementowany jako osobny fragment.

Fragment hosta w przykładzie 1 hostuje 2 fragmenty podrzędne, które tworzą ekran z podzielonym widokiem. Fragment hosta w przykładzie 2 hostuje pojedynczy fragment podrzędny, który tworzy wyświetlany fragment widoku przesuwanego.

Przy takiej konfiguracji każdy host ma powiązany z nim element FragmentManager, który zarządza jego fragmentami podrzędnymi. Przedstawiliśmy to na ilustracji 2 wraz z mapowaniem właściwości między elementami supportFragmentManager, parentFragmentManager i childFragmentManager.

z każdym hostem jest powiązany własny obiekt FragmentManager, który zarządza jego fragmentami podrzędnymi
Rysunek 2. Z każdym hostem jest powiązany własny zasób FragmentManager, który zarządza jego fragmentami podrzędnymi.

Właściwa właściwość FragmentManager, do której należy się odwołać, zależy od lokalizacji wywołania w hierarchii fragmentów oraz od menedżera fragmentów, do którego próbujesz uzyskać dostęp.

Gdy już będziesz mieć odwołanie do obiektu FragmentManager, możesz go używać do manipulowania fragmentami wyświetlanymi użytkownikowi.

Fragmenty podrzędne

Ogólnie rzecz biorąc, aplikacja składa się z jednej lub niewielkiej liczby działań w projekcie, a każde działanie reprezentuje grupę powiązanych ekranów. Aktywność może być punktem do miejsca nawigacji najwyższego poziomu oraz miejscem do określania zakresu obiektów ViewModel i innego stanu widoku między fragmentami. Fragment reprezentuje pojedyncze miejsce docelowe aplikacji.

Jeśli chcesz wyświetlać wiele fragmentów jednocześnie, np. w widoku podzielonym lub panelu, możesz użyć fragmentów podrzędnych zarządzanych przez fragment docelowy i jego menedżera fragmentów podrzędnych.

Inne przypadki użycia fragmentów podrzędnych:

  • Slajdy ekranu, wykorzystanie ViewPager2 we fragmencie nadrzędnym do zarządzania serią widoków fragmentów podrzędnych.
  • Nawigacja podrzędna w obrębie zbioru powiązanych ekranów.
  • Jetpack Navigation używa fragmentów podrzędnych jako pojedynczych miejsc docelowych. Aktywność hostuje 1 element nadrzędny NavHostFragment i wypełnia swój obszar różnymi podrzędnymi fragmentami miejsc docelowych, gdy użytkownicy poruszają się po aplikacji.

Użyj obiektu FragmentManager

FragmentManager zarządza stosem wstecznym fragmentów. W czasie działania FragmentManager może wykonywać operacje stosu wsteczne, takie jak dodawanie lub usuwanie fragmentów w odpowiedzi na interakcje użytkownika. Każdy zbiór zmian jest zatwierdzany razem jako jedna jednostka o nazwie FragmentTransaction. Bardziej szczegółowe informacje o transakcjach fragmentów znajdziesz w przewodniku po transakcjach fragmentów.

Gdy użytkownik kliknie przycisk Wstecz na urządzeniu lub wywołasz metodę FragmentManager.popBackStack(), transakcja dotycząca fragmentu najwyższego poziomu zostanie wyjęta ze stosu. Jeśli na stosie nie ma więcej transakcji na fragment, a nie używasz fragmentów podrzędnych, zdarzenie Back (Wstecz) wyświetla się w dymkach do aktywności. Jeśli używasz fragmentów podrzędnych, zapoznaj się ze specjalnymi uwagami na temat fragmentów podrzędnych i równorzędnych.

Gdy wywołujesz addToBackStack() w ramach transakcji, transakcja może zawierać dowolną liczbę operacji, takich jak dodawanie wielu fragmentów lub zastępowanie fragmentów w wielu kontenerach.

Po zablokowaniu stosu wszystkie te operacje odwracają jako jedno działanie niepodzielne. Jeśli jednak dodatkowe transakcje zostały zatwierdzone przed wywołaniem popBackStack() i nie użyto addToBackStack() w tej transakcji, operacje te nie mogą być cofnięte. Dlatego w obrębie jednego elementu FragmentTransaction unikaj przeplatania transakcji, które mają wpływ na stos wsteczny z tymi, które nie mają wpływu.

Realizacja transakcji

Aby wyświetlić fragment w kontenerze układu, użyj FragmentManager do utworzenia FragmentTransaction. W ramach transakcji możesz wykonać w kontenerze operację add() lub replace().

Na przykład prosta FragmentTransaction może wyglądać tak:

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack("name") // Name can be null
}

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack("name") // Name can be null
    .commit();

W tym przykładzie ExampleFragment zastępuje fragment (jeśli istnieje), który znajduje się obecnie w kontenerze układu identyfikowanym przez identyfikator R.id.fragment_container. Podanie klasy fragmentu do metody replace() pozwala FragmentManager na obsługę wystąpienia za pomocą FragmentFactory. Więcej informacji znajdziesz w sekcji Zależności między fragmentami.

setReorderingAllowed(true) optymalizuje zmiany stanu fragmentów zaangażowanych w transakcję, aby zapewnić prawidłowe działanie animacji i przejścia. Więcej informacji o poruszaniu się po animacji i przejściach znajdziesz w artykułach Transakcje fragmentów i Poruszanie się między fragmentami za pomocą animacji.

Wywołanie addToBackStack() przekazuje transakcję na stos wsteczny. Użytkownik może później cofnąć transakcję i przywrócić poprzedni fragment, klikając przycisk Wstecz. Jeśli w ramach 1 transakcji dodasz lub usuniesz wiele fragmentów, wszystkie te operacje są cofane po pojawieniu się stosu wstecznego. Opcjonalna nazwa podana w wywołaniu addToBackStack() umożliwia powrót do konkretnej transakcji za pomocą metody popBackStack().

Jeśli nie wywołasz funkcji addToBackStack() podczas wykonywania transakcji, która usuwa fragment, usunięty fragment zostanie zniszczony po zatwierdzeniu transakcji, a użytkownik nie będzie mógł do niego wrócić. Jeśli podczas usuwania fragmentu wywołasz metodę addToBackStack(), będzie on mieć tylko wartość STOPPED, a później RESUMED, gdy użytkownik wróci do poprzedniej wersji. W tym przypadku jego widok jest zniszczony. Więcej informacji znajdziesz w artykule Cykl życia fragmentu.

Znajdź istniejący fragment

Aby uzyskać odwołanie do bieżącego fragmentu w kontenerze układu, użyj findFragmentById(). Użyj metody findFragmentById(), aby wyszukać fragment według podanego identyfikatora w przypadku inflacji z pliku XML lub według identyfikatora kontenera dodanego w elemencie FragmentTransaction. Oto przykład:

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container)
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentById(R.id.fragment_container) as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null)
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();
...
ExampleFragment fragment =
        (ExampleFragment) fragmentManager.findFragmentById(R.id.fragment_container);

Możesz też przypisać do fragmentu unikalny tag i uzyskać odniesienie za pomocą findFragmentByTag(). Możesz przypisać tag za pomocą atrybutu XML android:tag we fragmentach zdefiniowanych w Twoim układzie lub w operacji add() bądź replace() w elemencie FragmentTransaction.

Kotlin

supportFragmentManager.commit {
   replace<ExampleFragment>(R.id.fragment_container, "tag")
   setReorderingAllowed(true)
   addToBackStack(null)
}
...
val fragment: ExampleFragment =
        supportFragmentManager.findFragmentByTag("tag") as ExampleFragment

Java

FragmentManager fragmentManager = getSupportFragmentManager();
fragmentManager.beginTransaction()
    .replace(R.id.fragment_container, ExampleFragment.class, null, "tag")
    .setReorderingAllowed(true)
    .addToBackStack(null)
    .commit();
...
ExampleFragment fragment = (ExampleFragment) fragmentManager.findFragmentByTag("tag");

Specjalne uwagi dotyczące fragmentów podrzędnych i równorzędnych

Tylko 1 element FragmentManager może w danym momencie kontrolować stos fragmentów. Jeśli w aplikacji wyświetlanych jest jednocześnie wiele fragmentów równorzędnych lub jeśli aplikacja korzysta z fragmentów podrzędnych, do obsługi głównej nawigacji w aplikacji wyznaczony jest jeden element FragmentManager.

Aby zdefiniować główny fragment nawigacji w transakcji dotyczącej fragmentu, wywołaj w transakcji metodę setPrimaryNavigationFragment() i przekażesz wystąpienie fragmentu, którego główną kontrolą ma parametr childFragmentManager.

Struktura nawigacji jest spójna z serią warstw, gdzie aktywność to najbardziej zewnętrzna warstwa, która otacza pod spodem każdą warstwę fragmentów podrzędnych. Każda warstwa ma jeden główny fragment nawigacji.

Gdy wystąpi zdarzenie Back (Wstecz), najlepsza warstwa steruje zachowaniem nawigacji. Gdy w najwyższej warstwie wewnętrznej nie ma już więcej transakcji dotyczących fragmentów, z których można wyskoczyć, kontrola wraca do następnej warstwy, a proces ten powtarza się, dopóki nie dotrzesz do działania.

Jeśli jednocześnie wyświetlane są co najmniej 2 fragmenty, tylko jeden z nich jest głównym fragmentem nawigacji. Ustawienie fragmentu jako głównego fragmentu nawigacji usuwa oznaczenie z poprzedniego fragmentu. W poprzednim przykładzie, jeśli ustawisz fragment szczegółów jako główny fragment nawigacyjny, oznaczenie głównego fragmentu zostanie usunięte.

Obsługa wielu wstecznych stosów

W niektórych przypadkach aplikacja może wymagać obsługi kilku stosów wstecznych. Typowym przykładem jest sytuacja, w której aplikacja korzysta z dolnego paska nawigacyjnego. FragmentManager umożliwia obsługę wielu stosów wstecznych za pomocą metod saveBackStack() i restoreBackStack(). Te metody umożliwiają przełączanie się między stosami wstecznymi przez zapisanie jednego stosu wstecznego i przywrócenie innego.

Funkcja saveBackStack() działa podobnie do wywoływania metody popBackStack() z opcjonalnym parametrem name: określona transakcja i wszystkie kolejne transakcje po niej na stosie są pobierane. Różnica polega na tym, że saveBackStack() zapisuje stan wszystkich fragmentów w transakcjach w wyskakującym okienku.

Załóżmy np., że wcześniej dodano do stosu wstecznego fragment przez wprowadzenie polecenia FragmentTransaction za pomocą metody addToBackStack(), jak w tym przykładzie:

Kotlin

supportFragmentManager.commit {
  replace<ExampleFragment>(R.id.fragment_container)
  setReorderingAllowed(true)
  addToBackStack("replacement")
}

Java

supportFragmentManager.beginTransaction()
  .replace(R.id.fragment_container, ExampleFragment.class, null)
  // setReorderingAllowed(true) and the optional string argument for
  // addToBackStack() are both required if you want to use saveBackStack()
  .setReorderingAllowed(true)
  .addToBackStack("replacement")
  .commit();

W takim przypadku możesz zapisać tę transakcję związaną z fragmentem i stan ExampleFragment, wywołując metodę saveBackStack():

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

Możesz wywołać restoreBackStack() z tym samym parametrem name, aby przywrócić wszystkie transakcje pobrane przez POP i wszystkie zapisane stany fragmentów:

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

Określanie zależności we fragmentach

Gdy dodajesz fragment, możesz ręcznie utworzyć jego instancję i dodać go do interfejsu FragmentTransaction.

Kotlin

fragmentManager.commit {
    // Instantiate a new instance before adding
    val myFragment = ExampleFragment()
    add(R.id.fragment_view_container, myFragment)
    setReorderingAllowed(true)
}

Java

// Instantiate a new instance before adding
ExampleFragment myFragment = new ExampleFragment();
fragmentManager.beginTransaction()
    .add(R.id.fragment_view_container, myFragment)
    .setReorderingAllowed(true)
    .commit();

Gdy zatwierdzisz transakcję związaną z fragmentem, używana będzie instancja utworzonego fragmentu. Jednak w wyniku zmiany konfiguracji Twoja aktywność i wszystkie jej fragmenty są niszczone, a następnie odtwarzane z najodpowiedniejszymi zasobami Androida. FragmentManager robi to za Ciebie: odtwarza wystąpienia Twoich fragmentów, dołącza je do hosta i odtwarza stan stosu wstecznego.

Domyślnie FragmentManager używa dostępnego przez platformę FragmentFactory do tworzenia nowej instancji fragmentu. Ta domyślna fabryka wykorzystuje odbicie do znalezienia i wywołania konstruktora bez argumentu dla danego fragmentu. Oznacza to, że nie można używać tej domyślnej fabryki do określania zależności między fragmentem. Oznacza to również, że dowolny niestandardowy konstruktor użyty do utworzenia fragmentu po raz pierwszy nie jest domyślnie używany podczas odtwarzania.

Aby podać zależności do fragmentu lub użyć dowolnego niestandardowego konstruktora, utwórz niestandardową podklasę FragmentFactory i zastąp ją FragmentFactory.instantiate. Następnie możesz zastąpić domyślną fabrykę urządzenia FragmentManager własną fabryką, która będzie używana do tworzenia instancji fragmentów.

Załóżmy, że masz obiekt DessertsFragment, który odpowiada za wyświetlanie popularnych deserów w Twoim rodzinnym mieście i że DessertsFragment jest uzależniony od klasy DessertsRepository, która dostarcza informacje potrzebne do wyświetlenia użytkownikowi prawidłowego interfejsu.

Możesz zdefiniować, że DessertsFragment wymaga wystąpienia DessertsRepository w jego konstruktorze.

Kotlin

class DessertsFragment(val dessertsRepository: DessertsRepository) : Fragment() {
    ...
}

Java

public class DessertsFragment extends Fragment {
    private DessertsRepository dessertsRepository;

    public DessertsFragment(DessertsRepository dessertsRepository) {
        super();
        this.dessertsRepository = dessertsRepository;
    }

    // Getter omitted.

    ...
}

Prosta implementacja FragmentFactory może wyglądać podobnie do tej.

Kotlin

class MyFragmentFactory(val repository: DessertsRepository) : FragmentFactory() {
    override fun instantiate(classLoader: ClassLoader, className: String): Fragment =
            when (loadFragmentClass(classLoader, className)) {
                DessertsFragment::class.java -> DessertsFragment(repository)
                else -> super.instantiate(classLoader, className)
            }
}

Java

public class MyFragmentFactory extends FragmentFactory {
    private DessertsRepository repository;

    public MyFragmentFactory(DessertsRepository repository) {
        super();
        this.repository = repository;
    }

    @NonNull
    @Override
    public Fragment instantiate(@NonNull ClassLoader classLoader, @NonNull String className) {
        Class<? extends Fragment> fragmentClass = loadFragmentClass(classLoader, className);
        if (fragmentClass == DessertsFragment.class) {
            return new DessertsFragment(repository);
        } else {
            return super.instantiate(classLoader, className);
        }
    }
}

W tym przykładzie podklasa FragmentFactory zastępuje metodę instantiate(), aby zapewnić niestandardową logikę tworzenia fragmentów dla DessertsFragment. Inne klasy fragmentów są obsługiwane przez domyślne zachowanie od FragmentFactory do super.instantiate().

Następnie możesz oznaczyć MyFragmentFactory jako fabrykę, która będzie używana do tworzenia fragmentów aplikacji, ustawiając właściwość w interfejsie FragmentManager. Musisz ustawić tę właściwość przed właściwością super.onCreate() działania, aby zapewnić, że MyFragmentFactory będzie używany podczas odtwarzania fragmentów.

Kotlin

class MealActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        supportFragmentManager.fragmentFactory = MyFragmentFactory(DessertsRepository.getInstance())
        super.onCreate(savedInstanceState)
    }
}

Java

public class MealActivity extends AppCompatActivity {
    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        DessertsRepository repository = DessertsRepository.getInstance();
        getSupportFragmentManager().setFragmentFactory(new MyFragmentFactory(repository));
        super.onCreate(savedInstanceState);
    }
}

Ustawienie FragmentFactory w aktywności zastępuje tworzenie fragmentów w całej hierarchii fragmentów aktywności. Innymi słowy, parametr childFragmentManager wszystkich dodanych fragmentów podrzędnych korzysta z ustawień fabryki fragmentów niestandardowych, chyba że zostanie zastąpione na niższym poziomie.

Testowanie za pomocą FragmentFactory

W ramach architektury pojedynczej aktywności przetestuj fragmenty w izolacji za pomocą klasy FragmentScenario. Ponieważ nie możesz polegać na niestandardowej logice onCreate swojej aktywności, możesz zamiast tego przekazać FragmentFactory jako argument w teście fragmentów, jak pokazano w tym przykładzie:

// Inside your test
val dessertRepository = mock(DessertsRepository::class.java)
launchFragment<DessertsFragment>(factory = MyFragmentFactory(dessertRepository)).onFragment {
    // Test Fragment logic
}

Szczegółowe informacje o tym procesie testowania i pełne przykłady znajdziesz w artykule Testowanie fragmentów.