Gestionnaire de fragments

FragmentManager est la classe chargée d'effectuer des actions au niveau des fragments de votre application, comme les ajouter, les supprimer ou les remplacer, et les ajouter à la pile "Retour".

Il est possible que vous n'interagissiez jamais directement avec FragmentManager si vous utilisez la bibliothèque Jetpack Navigation, qui utilise FragmentManager en votre nom. Cependant, toute application qui implique des fragments utilise FragmentManager à un certain niveau. Il est donc important de comprendre ce en quoi il consiste et comment il fonctionne.

Cette page aborde les sujets suivants :

  • Comment accéder à FragmentManager
  • Le rôle de FragmentManager par rapport à vos activités et fragments
  • Comment gérer la pile "Retour" avec FragmentManager
  • Comment fournir des données et des dépendances à vos fragments

Accéder à FragmentManager

Vous pouvez accéder à FragmentManager à partir d'une activité ou d'un fragment.

FragmentActivity et ses sous-classes, telles que AppCompatActivity, ont accès à FragmentManager via la méthode getSupportFragmentManager().

Les fragments peuvent héberger un ou plusieurs fragments enfants. Dans un fragment, vous pouvez obtenir une référence à FragmentManager qui gère les enfants du fragment via getChildFragmentManager(). Si vous devez accéder à son hôte FragmentManager, vous pouvez utiliser getParentFragmentManager().

Voici quelques exemples pour voir les relations entre les fragments, leurs hôtes et les instances FragmentManager associées à chacun.

Deux exemples de mise en page d'interface utilisateur montrant les relations entre les fragments et leurs activités hôtes
Figure 1 : Deux exemples de mise en page d'interface utilisateur montrant les relations entre les fragments et leurs activités hôtes

La figure 1 illustre deux exemples, chacun associé à un seul hôte d'activité. Dans ces deux exemples, l'activité hôte affiche la navigation de premier niveau pour l'utilisateur en tant qu'élément BottomNavigationView responsable du remplacement du fragment hôte par différents écrans dans l'application, chaque écran étant implémenté en tant que fragment distinct.

Le fragment hôte dans l'exemple 1 héberge deux fragments enfants qui constituent un écran avec vue fractionnée. Le fragment hôte de l'exemple 2 héberge un seul fragment enfant qui constitue le fragment d'affichage d'une vue par balayage.

Compte tenu de cette configuration, chaque hôte peut être associé à un FragmentManager qui gère ses fragments enfants. Ceci est illustré dans la figure 2, qui montre les mappages de propriétés entre supportFragmentManager, parentFragmentManager et childFragmentManager.

Chaque hôte est associé à un FragmentManager qui gère ses fragments enfants.
Figure 2 : Chaque hôte est associé à un élément FragmentManager qui gère ses fragments enfants

La propriété FragmentManager appropriée à référencer dépend de l'emplacement du site d'appel dans la hiérarchie des fragments et du gestionnaire de fragment auquel vous essayez d'accéder.

Une fois que vous avez une référence à l'élément FragmentManager, vous pouvez l'utiliser pour manipuler les fragments présentés à l'utilisateur.

Fragments enfants

En règle générale, votre application comporte une seule activité ou un petit nombre d'activités dans votre projet d'application, chaque activité représentant un groupe d'écrans associés. L'activité peut fournir un endroit où placer la navigation de niveau supérieur et un endroit permettant d'appliquer les objets ViewModel et d'autres états d'affichage entre les fragments. Un fragment représente une destination individuelle dans votre application.

Si vous souhaitez afficher plusieurs fragments à la fois, par exemple dans une vue fractionnée ou un tableau de bord, vous pouvez utiliser des fragments enfants gérés par votre fragment de destination et son gestionnaire de fragments enfants.

Voici d'autres cas d'utilisation de fragments enfants :

  • Diapositives d'écran, avec un élément ViewPager2 dans un fragment parent permettant de gérer une série de vues de fragments enfants.
  • Sous-navigation dans un ensemble d'écrans associés.
  • Jetpack Navigation utilise des fragments enfants comme destinations individuelles. Une activité héberge un seul élément NavHostFragment parent et remplit son espace avec différents fragments de destination enfants lorsque les utilisateurs parcourent votre application.

Utiliser FragmentManager

FragmentManager gère la pile "Retour" des fragments. Lors de l'exécution, l'instance FragmentManager peut effectuer des opérations de pile "Retour" telles que l'ajout ou la suppression de fragments en réponse aux interactions utilisateur. Chaque ensemble de modifications est validé dans une seule et même unité appelée FragmentTransaction. Pour obtenir des informations plus détaillées sur les transactions de fragment, consultez ce guide.

Lorsque l'utilisateur appuie sur le bouton "Retour" de son appareil ou lorsque vous appelez FragmentManager.popBackStack(), la transaction de fragment la plus élevée sort de la pile. Si la pile ne contient plus de transactions de fragment et que vous n'utilisez pas de fragments enfants, l'événement "Retour" s'affiche dans l'activité. Si vous utilisez des fragments enfants, découvrez les points à prendre en compte concernant les fragments enfants et frères.

Lorsque vous appelez addToBackStack() au niveau d'une transaction, celle-ci peut inclure un nombre illimité d'opérations, comme l'ajout de plusieurs fragments ou le remplacement de fragments dans plusieurs conteneurs.

Lorsque la pile "Retour" est renvoyée, toutes ces opérations s'inversent comme une seule action atomique. Toutefois, si vous avez validé des transactions supplémentaires avant l'appel de popBackStack() et que vous n'avez pas utilisé addToBackStack() pour la transaction, ces opérations ne sont pas annulées. Par conséquent, dans un seul élément FragmentTransaction, évitez d'entrelacer les transactions qui affectent la pile "Retour" avec celles qui ne l'affectent pas.

Effectuer une transaction

Pour afficher un fragment dans un conteneur de mise en page, utilisez FragmentManager afin de créer un élément FragmentTransaction. Dans la transaction, vous pouvez ensuite effectuer une opération add() ou replace() au niveau du conteneur.

Par exemple, un élément FragmentTransaction simple peut se présenter comme suit :

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();

Dans cet exemple, ExampleFragment remplace le fragment, le cas échéant, qui se trouve actuellement dans le conteneur de mise en page identifié par l'ID R.id.fragment_container. Fournir la classe du fragment à la méthode replace() permet à FragmentManager de gérer l'instanciation à l'aide de son élément FragmentFactory. Pour en savoir plus, consultez la section Fournir des dépendances à vos fragments.

setReorderingAllowed(true) optimise les changements d'état des fragments impliqués dans la transaction afin que les animations et les transitions fonctionnent correctement. Pour en savoir plus sur la navigation à l'aide d'animations et de transitions, consultez les sections Transactions de fragment et Parcourir les fragments à l'aide d'animations.

L'appel de addToBackStack() rajoute la transaction dans la pile "Retour". L'utilisateur peut ensuite annuler la transaction et récupérer le fragment précédent en appuyant sur le bouton Retour. Si vous avez ajouté ou supprimé plusieurs fragments en une seule transaction, toutes ces opérations sont annulées lorsque la pile "Retour" est renvoyée. Le nom facultatif fourni dans l'appel addToBackStack() vous permet de revenir à cette transaction spécifique à l'aide de popBackStack().

Si vous n'appelez pas addToBackStack() lorsque vous effectuez une transaction qui supprime un fragment, celui-ci est détruit lorsque la transaction est validée. L'utilisateur ne peut pas y revenir. Si vous appelez addToBackStack() lors de la suppression d'un fragment, celui-ci est uniquement arrêté (STOPPED), puis redémarré (RESUMED) lorsque l'utilisateur revient en arrière. Sa vue est détruite dans ce cas. Pour en savoir plus, consultez la section Cycle de vie des fragments.

Rechercher un fragment

Vous pouvez obtenir une référence au fragment actuel dans un conteneur de mise en page à l'aide de la commande findFragmentById(). Utilisez findFragmentById() pour rechercher un fragment en fonction de l'ID donné en cas de gonflement à partir du code XML ou de l'ID de conteneur lorsqu'il a été ajouté dans une propriété FragmentTransaction. Exemple :

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

Vous pouvez également attribuer une balise unique à un fragment et obtenir une référence à l'aide de findFragmentByTag(). Vous pouvez attribuer une balise à l'aide de l'attribut XML android:tag au niveau des fragments définis dans votre mise en page, ou lors d'une opération add() ou replace() dans un élément 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");

Points à prendre en compte concernant les fragments enfants et frères

Un seul élément FragmentManager peut contrôler la pile "Retour" d'un fragment. Si votre application affiche plusieurs fragments frères à l'écran en même temps ou si elle utilise des fragments enfants, un élément FragmentManager est désigné pour gérer la navigation principale de votre application.

Pour définir le fragment de navigation principal dans une transaction de fragment, appelez la méthode setPrimaryNavigationFragment() au niveau de la transaction, en transmettant l'instance du fragment dont childFragmentManager a le contrôle principal.

Considérez la structure de navigation comme une série de couches, l'activité étant la couche externe encapsulant chaque couche sous-jacente de fragments enfants. Chaque couche comporte un seul fragment de navigation principal.

Lorsque l'événement "Retour" se produit, la couche interne contrôle le comportement de navigation. Une fois que la couche interne ne contient plus de transactions de fragment exploitables, la commande retourne dans la couche suivante, et ce processus se répète jusqu'à ce que vous atteigniez l'activité.

Lorsque deux fragments ou plus sont affichés en même temps, seul l'un d'entre eux est le fragment de navigation principal. La définition d'un fragment comme fragment de navigation principal supprime la désignation du fragment précédent. Dans l'exemple précédent, si vous définissez le fragment de détail comme fragment de navigation principal, la désignation du fragment principal est supprimée.

Prendre en charge plusieurs piles "Retour"

Dans certains cas, il se peut que votre application doive utiliser plusieurs piles "Retour". C'est par exemple le cas si elle utilise une barre de navigation inférieure. FragmentManager vous permet d'utiliser plusieurs piles "Retour" avec les méthodes saveBackStack() et restoreBackStack(). Ces méthodes vous permettent de passer d'une pile "Retour" à l'autre en enregistrant une pile et en en restaurant une autre.

saveBackStack() fonctionne de la même manière que l'appel de popBackStack() avec le paramètre facultatif name : la transaction spécifiée et toutes les transactions ultérieures dans la pile sont renvoyées. La différence est que saveBackStack() enregistre l'état de tous les fragments dans les transactions transmises.

Par exemple, supposons que vous ayez précédemment ajouté un fragment à la pile "Retour" en rajoutant un élément FragmentTransaction avec addToBackStack(), comme illustré dans l'exemple suivant :

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();

Dans ce cas, vous pouvez enregistrer cette transaction de fragment et l'état de l'élément ExampleFragment en appelant saveBackStack() :

Kotlin

supportFragmentManager.saveBackStack("replacement")

Java

supportFragmentManager.saveBackStack("replacement");

Vous pouvez appeler restoreBackStack() avec le même paramètre de nom pour restaurer toutes les transactions transmises et tous les états de fragment enregistrés :

Kotlin

supportFragmentManager.restoreBackStack("replacement")

Java

supportFragmentManager.restoreBackStack("replacement");

Fournir des dépendances à vos fragments

Lorsque vous ajoutez un fragment, vous pouvez l'instancier manuellement et l'ajouter à l'élément 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();

Lorsque vous validez la transaction de fragment, l'instance du fragment que vous avez créé est l'instance utilisée. Toutefois, lors d'une modification de la configuration, votre activité et tous ses fragments sont détruits, puis recréés avec les ressources Android les plus pertinentes. FragmentManager gère tout cela pour vous : il recrée les instances de vos fragments, les associe à l'hôte et recrée l'état de la pile "Retour".

Par défaut, FragmentManager utilise une classe FragmentFactory fournie par le framework pour instancier une nouvelle instance de votre fragment. Cette fabrique par défaut utilise la réflexion afin de rechercher et d'appeler un constructeur sans argument pour votre fragment. En d'autres termes, vous ne pouvez pas utiliser cette fabrique par défaut pour fournir des dépendances à votre fragment. Cela signifie également que tout constructeur personnalisé que vous avez utilisé pour créer votre fragment la première fois ne sera pas utilisé par défaut lors de l'opération de recréation.

Pour fournir des dépendances à votre fragment ou utiliser un constructeur personnalisé, créez une sous-classe FragmentFactory personnalisée, puis remplacez FragmentFactory.instantiate. Vous pouvez ensuite remplacer la fabrique par défaut de FragmentManager par votre fabrique personnalisée, qui sera ensuite utilisée pour instancier vos fragments.

Supposons que vous ayez un élément DessertsFragment chargé d'afficher les desserts les plus populaires dans votre ville natale et que DessertsFragment dépende d'une classe DessertsRepository qui lui fournit les informations nécessaires pour présenter l'interface appropriée à l'utilisateur.

Vous pouvez définir l'élément DessertsFragment pour qu'il exige une instance DessertsRepository dans son constructeur.

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.

    ...
}

Une implémentation simple de FragmentFactory peut ressembler à ce qui suit.

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

Cet exemple montre la sous-classe FragmentFactory, qui remplace la méthode instantiate() afin de fournir une logique de création de fragments personnalisée pour un élément DessertsFragment. Les autres classes de fragment sont gérées par le comportement par défaut de FragmentFactory via super.instantiate().

Vous pouvez ensuite désigner MyFragmentFactory comme fabrique à utiliser pour créer les fragments de votre application, en définissant une propriété au niveau de FragmentManager. Vous devez définir cette propriété avant l'objet super.onCreate() de votre activité pour vous assurer que MyFragmentFactory est utilisé lors de la recréation de vos fragments.

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

La définition de FragmentFactory dans l'activité remplace la création de fragments dans toute la hiérarchie des fragments de l'activité. En d'autres termes, le fragment childFragmentManager de tous les fragments enfants que vous ajouterez utilisera la configuration de fabrique de fragments personnalisée définie ici, sauf s'il est remplacé à un niveau inférieur.

Tester les fragments avec FragmentFactory

Dans l'architecture d'une activité unique, testez vos fragments de manière isolée à l'aide de la classe FragmentScenario. Comme vous ne pouvez pas compter sur la logique onCreate personnalisée de votre activité, vous pouvez transmettre FragmentFactory en tant qu'argument au test de vos fragments, comme illustré dans l'exemple suivant :

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

Pour obtenir des informations détaillées sur ce processus de test et obtenir des exemples complets, consultez la section Tester vos fragments.