Guide pratique des Activity et Fragment Android : Cycle de vie, changements de configuration, pile de tâches, modes de lancement et autorisations

Guide pratique des Activity et Fragment Android : Cycle de vie, changements de configuration, pile de tâches, modes de lancement et autorisations

Cet article se concentre sur plusieurs éléments essentiels de l'interface utilisateur et de la navigation Android : le cycle de vie des Activity, la gestion des changements de configuration et de l'état, la pile de tâches et les modes de lancement, l'utilisation statique/dynamique des Fragment et leur cycle de vie, ainsi que la gestion des autorisations dynamiques. Le contenu est organisé selon le schéma « concept → utilisation → points de vigilance/courants piégés → exemples de code », accompagné d'extraits de code de projets réels pour aider les lecteurs à implémenter correctement ces concepts dans des applications professionnelles.

Table des matières

  • Introduction
    1. Cycle de vie des Activity
  • 1.1 Principales méthodes de rappel et leurs responsabilités
  • 1.2 Chemins de cycle de vie courants
  • 1.3 Diagramme du cycle de vie
    1. Changements de configuration (Configuration Changes)
  • 2.1 Comportement par défaut : reconstruction de l'Activity
  • 2.2 Gestion personnalisée des changements de configuration : configChanges et onConfigurationChanged
  • 2.3 Restriction de l'orientation de l'écran
    1. Sauvegarde et restauration de l'état de l'interface
  • 3.1 Quand onSaveInstanceState est déclenché
  • 3.2 Sauvegarde : onSaveInstanceState
  • 3.3 Restauration : onCreate vs onRestoreInstanceState
    1. Pile de tâches, modes de lancement et drapeaux Intent
  • 4.1 Bases de la pile de tâches (Task Stack)
  • 4.2 Comparaison et sélection des modes de lancement
  • 4.3 Combinaisons courantes de drapeaux Intent
  • 4.4 Android 12 : singleInstancePerTask
    1. Réutilisation de mise en page : include
    1. Bases des Fragment et leur gestion dynamique
  • 6.1 Ajout statique de Fragment
  • 6.2 Ajout/suppression/remplacement dynamique : FragmentManager
  • 6.3 Points clés du cycle de vie des Fragment
    1. Communication entre Activity et Fragment
  • 7.1 Passage de valeurs de l'Activity vers le Fragment
  • 7.2 Retour de résultats du Fragment vers l'Activity
  • Conclusion

Introduction

En développement Android, "la navigation entre écrans" et "l'état de l'interface" sont souvent liés : perte du contenu des champs de texte après la rotation de l'écran, accumulation d'Activity dupliquées dans la pile lors du retour depuis une notification, pointeurs nuls lors du transfert de données depuis un Fragment, etc. Ces problèmes sont tous liés au cycle de vie + pile de tâches + sauvegarde d'état + communication entre composants.

1. Cycle de vie des Activity

Le cycle de vie d'une Activity décrit le processus complet de sa création à sa destruction. Comprendre la responsabilité de chaque rappel permet de correctement associer les états "initialisation, visible, interactif, en pause, arrêté, détruit" à la logique métier.

1.1 Méthodes de rappel principales et leurs responsabilités

Méthode Scénario typique de déclenchement Usage approprié Points de vigilance courants
onCreate() Première création ou "reconstruction" setContentView, initialisation des membres, liaison des vues, abonnements aux données Éviter les opérations coûteuses (impact sur la première frame)
onStart() L'Activity devient visible Logique légère de visibilité Visible mais pas forcément interactive
onResume() Au premier plan et interactive Démarrage d'animations, caméra/capteurs "ressources de premier plan" L'utilisateur peut vraiment interagir à partir d'ici
onPause() Perd le focus au premier plan (peut rester visible) Rapide sauvegarde d'état critique, pause d'animations/caméra Doit être rapide : un blocage cause un gel du changement d'écran
onStop() Complètement invisible Libération de ressources lourdes, annulation des écouteurs Le processus peut encore être actif, les variables membres peuvent ne pas être perdues
onDestroy() Sur le point d'être détruit Nettoyage final (sécurité) Peut être déclenché par finish() ou回收/重建 du système

De plus, lorsqu'une Activity passe de l'état Stopped à l'avant-plan, elle déclenche généralement : onRestart() (avant onStart()).

1.2 Chemins de cycle de vie courants

  • Démarrage d'une page : onCreate → onStart → onResume
  • Ouverture d'une nouvelle page recouvrant la page actuelle : page actuelle onPause (nouvelle page onCreate/onStart/onResume) ; si la page actuelle est complètement invisible, elle continue avec onStop
  • Appui sur Home pour retourner au bureau : onPause → onStop (pas forcément immédiatement onDestroy)
  • Retour depuis l'arrière-plan au premier plan : onRestart → onStart → onResume
  • Appui sur le bouton Retour pour terminer la page : onPause → onStop → onDestroy

1.3 Diagramme du cycle de vie

2. Changements de configuration (Configuration Changes)

Les changements de configuration se produisent lorsque l'environnement d'exécution change : rotation de l'écran, changement de langue/région, mode sombre, changement de taille de fenêtre (écran divisé/écran pliable). L'impact le plus important est : l'Activity est-elle reconstruite.

2.1 Comportement par défaut : reconstruction de l'Activity

Dans la plupart des cas de changement de configuration, le système détruit l'ancienne instance et en crée une nouvelle. Pour que la nouvelle instance restaure l'état de l'utilisateur, le système appelle onSaveInstanceState() pour sauvegarder un Bundle d'état, et le transmet à onCreate() / onRestoreInstanceState() lors de la reconstruction :

Ainsi, après une rotation d'écran, les variables membres peuvent être perdues, ainsi que les saisies de l'utilisateur. Il est nécessaire d'utiliser la sauvegarde/restauration d'état ou des méthodes plus robustes de conservation des données.

2.2 Gestion personnalisée des changements de configuration : configChanges et onConfigurationChanged

On peut déclarer configChanges dans le fichier manifeste pour que le système ne reconstruise pas l'Activity, mais appelle plutôt onConfigurationChanged() pour que le développeur gère cela manuellement. Voici la déclaration de ConfigChangesActivity dans un projet :

<activity
    android:name=".activite.ConfigChangesActivity"
    android:configChanges="orientation|screenSize"
    android:exported="true" />


Points de vigilance :

  • configChanges contourne le mécanisme de reconstruction automatique et de rechargement des ressources du système (par exemple layout-land, ressources de mode nuit, etc.), et nécessite un travail d'adaptation manuel.
  • Pour les pages d'application standard, il est plus recommandé : accepter la reconstruction du système + sauvegarder/restaurer correctement l'état. Seules les pages qui nécessitent un contrôle total devraient utiliser configChanges.

2.3 Restriction de l'orientation de l'écran

Si le métier le permet, on peut restreindre l'orientation via screenOrientation pour réduire les reconstructions déclenchées par la rotation. Valeurs courantes : portrait (portrait), landscape (paysage), sensor (suivi du capteur).

Dans le fichier AndroidManifest.xml, on peut définir l'orientation d'une Activity avec l'attribut android:screenOrientation :

  • portrait : autorise uniquement le mode portrait.
  • landscape : autorise uniquement le mode paysage.
  • sensor : bascule automatiquement l'écran en fonction du capteur de direction physique.
  • user : détermine l'orientation en fonction des paramètres d'écran de l'utilisateur.
  • nosensor : ignore le capteur de direction et utilise l'orientation d'affichage actuelle.

3. Sauvegarde et restauration de l'état de l'interface

Lorsqu'une Activity est reconstruite en raison d'un changement de configuraton, ou lorsqu'elle est recréée après avoir été supprimée en arrière-plan, l'interface doit être redessinée. Pour éviter la perte des saisies utilisateur et des états UI critiques, il faut sauvegarder et restaurer ces états lors de la reconstruction.

3.1 Quand onSaveInstanceState est déclenché

onSaveInstanceState(Bundle outState) a pour objectif principal de sauvegarder l'état instantané léger lorsque "le système pourrait avoir besoin de reconstruire l'Activity ultérieurement". Scénarios typiques :

  • Reconstruction de l'Activity due à un changement de configuration (par exemple rotation de l'écran)
  • L'Activity passe en arrière-plan, et le système peut la détruire pour libérer de la mémoire ; lorsque l'utilisateur revient, l'état doit être restauré

Il est déconseillé de placer de grandes quantités de données ou d'objets volumineux dans le Bundle, car cela pourrait causer des exceptions à l'exécution. Les données complexes sont plus adaptées à une base de données, un fichier, un ViewModel ou un cache réseau.

3.2 Sauvegarde : onSaveInstanceState

Voici un exemple d'Activity qui sauvegarde et restaure les données saisies :

/**
 * Sauvegarde des données d'Activity (sauvegarde + restauration)
 */
public class SauvegardeDonneesActivite extends AppCompatActivity {

    private static final String TAG = "SauvegardeDonneesActivite";

    private String age;
    private String pseudo;
    private EditText edPseudo;
    private EditText edAge;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activite_sauvegarde_donnees);
        // L'Activity n'est pas créée pour la première fois, mais potentiellement pour restaurer des données
        if (savedInstanceState != null) {
            // Restaurer les données
            pseudo = savedInstanceState.getString("cle_pseudo");
            age = savedInstanceState.getString("cle_age");

            Log.i(TAG, "onCreate: pseudo = " + pseudo);
            Log.i(TAG, "onCreate: age = " + age);
        }

        edPseudo = findViewById(R.id.et_pseudo);
        edAge = findViewById(R.id.et_age);

        findViewById(R.id.btn_valider).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                // Après validation, récupérer les valeurs des champs de saisie
                pseudo = edPseudo.getText().toString().trim();
                age = edAge.getText().toString().trim();

            }
        });
        Log.i(TAG, "onCreate: ");
    }

    /**
     * Sauvegarde des données
     * 1. Lorsque l'Activity entre en arrière-plan, elle peut être détruite ; le système appelle cette méthode avant destruction
     * 2. Lorsqu'un changement de configuration se produit, cette méthode est appelée
     * 3. Lorsque l'utilisateur appuie sur Home, reçoit un appel, etc.
     */
    @Override
    protected void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        // Sauvegarder les données
        outState.putString("cle_pseudo", pseudo);
        outState.putString("cle_age", age);

        Log.i(TAG, "onSaveInstanceState: Sauvegarde des données");
    }

    /**
     * Restauration des données
     *
     * @param savedInstanceState
     */
    @Override
    protected void onRestoreInstanceState(@NonNull Bundle savedInstanceState) {
        super.onRestoreInstanceState(savedInstanceState);
        // Restaurer les données
        pseudo = savedInstanceState.getString("cle_pseudo");
        age = savedInstanceState.getString("cle_age");

        // Restaurer l'affichage de l'interface
        edPseudo.setText(pseudo);
        edAge.setText(age);

        Log.i(TAG, "onRestoreInstanceState: ");
    }
}


3.3 Restauration : onCreate vs onRestoreInstanceState

Point d'entrée Moment d'appel Scénario d'utilisation Point clé
onCreate(Bundle) Plus tôt Le flux d'initialisation dépend de l'état Vérification de nullité nécessaire : savedInstanceState != null
onRestoreInstanceState(Bundle) Plus tard, et seulement si un état existe Restaurer après que l'UI soit prête (par exemple afffectation directe à des contrôles) L'argument n'est pas null, mais il faut s'assurer que la Vue est déjà initialisée

Pratiques courantes :

  • Pour des états simples, la restauration dans onCreate() avec vérification de nullité suffit.
  • Lorsque la restauration dépend de findViewById déjà effectué, on peut le faire dans onRestoreInstanceState() ou onViewCreated (pour les Fragment).

4. Pile de tâches, modes de lancement et drapeaux Intent

Le comportement de navigation d'une Activity est déterminé par trois éléments :

  1. La pile de tâches (Task Stack) : le "conteneur" des Activity, avec un comportement typique de dernier entré, premier sorti (LIFO)
  2. Le mode de lancement (launchMode) : défini dans le manifeste, détermine la réutilisation d'instance et le comportement dans la pile
  3. Les drapeaux Intent (Intent Flags) : définis dynamiquement au lancement, permettent un contrôle fin "pour ce seul saut"

4.1 Bases de la pile de tâches (Task Stack)

  • Démarrage d'une Activity : empilement (push)
  • Appui sur Retour/finish() : dépilement (pop)
  • Le système peut avoir plusieurs piles de tâches (par exemple démarrage depuis une icône du bureau, une notification, ou une application externe)

4.2 Comparaison et sélection des modes de lancement

L'exemple suivant montre le comportement "création en double" du mode de lancement par défaut standard :

public class ActiviteStandard extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activite_standard);


        findViewById(R.id.btn_demarrer).setOnClickListener(view ->
                startActivity(new Intent(this, ActiviteStandard.class)));
    }

}


<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/principal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activite.lancementmode.ActiviteStandard">

    <Button
        android:id="@+id/btn_demarrer"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Démarrer soi-même"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


Mode de lancement standard

  • standard est le mode de lancement par défaut. Chaque fois qu'une Activity est démarrée, une nouvelle instance de cette Activity est créée et placée dans la pile de tâches.
  • Scénario d'exemple : dans une application d'actualités, lorsque l'utilisateur clique sur un article dans la liste, l'application démarre une Activity pour afficher les détails de l'article. Si l'utilisateur clique sur plusieurs articles différents, le système créera une nouvelle instance de l'Activity pour chaque article. Chaque clic ajoutera une nouvelle instance de l'Activity des détails d'article dans la pile.

Mode de lancement singleTop

  • Le mode singleTop est similaire à standard, mais avec une différence clé : si l'Activity demandée se trouve déjà en haut de la pile de tâches, aucune nouvelle instance n'est créée ; l'instance existante est réutilisée et la méthode onNewIntent() de cette instance est appelée.

Mode de lancement singleTask

  • Le mode singleTask garantit qu'il n'y a qu'une seule instance de cette Activity dans la pile de tâches. Si cette Activity existe déjà dans la pile, où qu'elle se trouve, le système la place en haut de la pile et supprime toutes les Activity au-dessus d'elle. Si l'Activity principale existe déjà dans la pile, le système la déplacera en haut et détruira les autres Activity au-dessus.

Mode de lancement singleInstance

  • singleInstance est le mode de lancement le plus spécial. Il non seulement garantit qu'il n'y a qu'une seule instance de cette Activity, mais la place également dans une pile de tâches indépendante. Les Activity d'autres applications ne partageront pas cette pile de tâches.

Mode de lancement singleInstancePerTask

  • Introduit dans Android 12, ce mode est spécifiquement conçu pour les scénarios multi-tâches et multi-fenêtres. Il permet une instance unique de cette Activity par pile de tâches, avec plusieurs instances possibles dans différentes piles. On peut le considérer comme une version améliorée de singleInstance.

Comparaison des comportements courants des modes de lancement :

Mode de lancement Autorise les instances multiples Condition de réutilisation Comportement dans la pile Scénario typique
standard (par défaut) Non réutilisé Crée une nouvelle instance à chaque démarrage Pages courantes/détails
singleTop (dans la plupart des cas) Cible déjà en haut de la pile Réutilise l'instance en haut de la pile et appelle onNewIntent() Éviter les doublons en haut de la pile
singleTask (maximum un par pile) Existe déjà dans la pile Supprime ce qui est au-dessus et place en haut Page d'acccueil/point d'entrée
singleInstance (exclusif) Toujours unique Pile de tâches indépendante, non partagée Appel vidéo/liseuse "expérience isolée"

Configuration dans le manifeste :

<activity
    android:name=".activite.lancementmode.ActiviteStandard"
    android:exported="true"
    android:launchMode="singleTask" />


4.3 Combinaisons courantes de drapeaux Intent

Les flags de Intent permettent un contrôle fin d'un seul saut. L'exemple suivant utilise FLAG_ACTIVITY_NEW_TASK :

public class ActiviteLancement1 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activite_lancement_mode1);
        findViewById(R.id.text_view).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(ActiviteLancement1.this, ActiviteLancement2.class);
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
                startActivity(intent);
            }
        });
    }

    @Override
    protected void onNewIntent(Intent intent) {
        super.onNewIntent(intent);
    }
}


public class ActiviteLancement2 extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        setContentView(R.layout.activite_lancement_mode2);


        findViewById(R.id.text_view).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Intent intent = new Intent(ActiviteLancement2.this, ActiviteStandard.class);

                startActivity(intent);
            }
        });
    }
}


Drapeau Comportement principal Usage courant
FLAG_ACTIVITY_NEW_TASK Démarrage dans une nouvelle pile de tâches existante ou nouvelle Démarrage depuis Service/Receiver, démarrage inter-tâches
FLAG_ACTIVITY_SINGLE_TOP Réutilisation si la cible est en haut de la pile Éviter les doublons en haut de la pile
FLAG_ACTIVITY_CLEAR_TOP Si la cible est dans la pile, supprime ce qui est au-dessus et appelle onNewIntent() Retour à une page et vidage de la pile
FLAG_ACTIVITY_REORDER_TO_FRONT Déplace une instance existante au sommet de la pile sans la supprimer Ramener une page précédemment ouverte au premier plan

4.4 Android 12 : singleInstancePerTask

singleInstancePerTask (introduit dans Android 12) est adapté aux multi-fenêtres/multi-tâches : maximum une instance par pile de tâches, mais différentes piles peuvent avoir各自的实例.

5. Réutilisation de mise en page : include

Lorsque plusieurs pages partagent un segment d'UI, on peut l'extraire en une mise en page indépendante et la réutiliser via <include>, réduisant ainsi la duplication de code et le coût de maintenance des styles.

Extrait réutilisable layout_bleu :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:background="#1082ff">

    <TextView
        android:id="@+id/tv"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Segment UI avec fond bleu"
        android:textColor="@color/white"
        android:textSize="30sp" />

</RelativeLayout>


Réutilisation dans la mise en page de l'Activity :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/principal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    tools:context=".activite.IncludeLayoutActivity">

    <include
        android:id="@+id/include_bleu"
        layout="@layout/layout_bleu" />

    <include layout="@layout/activite_changements_config" />

</LinearLayout>


On peut accéder directement aux contrôles enfants de include dans le code :

public class ActiviteIncludeLayout extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activite_include_layout);

        TextView tvlabel = findViewById(R.id.tv_label);
        tvlabel.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                tvlabel.setText("ActiviteIncludeLayout");
            }
        });
    }
}


Problèmes courants :

  • Si les mises en page include ont des id dupliqués, et qu'une même page inclut plusieurs fois le même layout, findViewById() peut récupérer "le contrôle inattendu". Dans ces cas, il est préférable d'utiliser <merge> ou le binding de vues (ViewBinding/DataBinding) pour une référence plus sûre.

6. Bases des Fragment et leur gestion dynamique

Un Fragment est un composant UI pouvant être intégré dans une Activity, avec son propre cycle de vie. Les utilisations courantes sont : ajout statique et gestion dynamique.

6.1 Ajout statique de Fragment

L'ajout statique consiste à déclarer directement le conteneur Fragment et sa classe dans le XML de l'Activity. Exemple :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/principal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activite.FragmentTestActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/mon_fragment"
        android:name="com.ls.acbjp.activite.MonFragment"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>


Le MonFragment correspondant :

public class MonFragment extends Fragment {

    private static final String TAG = "MonFragment";

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        // Appelé lors de la création de la hiérarchie de vues du Fragment. Doit retourner la vue racine du Fragment ici.
        View vue = inflater.inflate(R.layout.layout_rouge, container, false);
        return vue;
    }
}


La mise en page du Fragment layout_rouge :

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="200dp"
    android:background="#ff0000">

    <TextView
        android:id="@+id/tv_texte"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_centerInParent="true"
        android:text="Segment UI avec fond rouge"
        android:textColor="@color/white"
        android:textSize="30sp" />

</RelativeLayout>


Points clés de l'ajout statique :

  • android:id est utilisé par le système pour gérer l'instance du Fragment (récupération/reconstruction/opérations de transaction)
  • android:name spécifie la classe de Fragment à instancier

6.2 Ajout/suppression/remplacement dynamique : FragmentManager

Le cœur de la gestion dynamique est : commencer une transaction (beginTransaction()) via FragmentManager, exécuter add/remove/replace, et enfin commit().

La mise en page fournit un conteneur FragmentContainerView :

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/principal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".activite.FragmentActivity">

    <!-- Ajout statique de Fragment -->
    <!--        <androidx.fragment.app.FragmentContainerView-->
    <!--            android:id="@+id/mon_fragment"-->
    <!--            android:name="com.ls.acbjp.activite.MonFragment"-->
    <!--            android:layout_width="match_parent"-->
    <!--            android:layout_height="wrap_content"-->
    <!--            app:layout_constraintStart_toStartOf="parent"-->
    <!--            app:layout_constraintTop_toTopOf="parent" />-->

    <Button
        android:id="@+id/btn_ajouter"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Ajouter Fragment"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />


    <Button
        android:id="@+id/btn_supprimer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Supprimer Fragment"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_ajouter" />

    <Button
        android:id="@+id/btn_remplacer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Remplacer Fragment"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_supprimer" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fcv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@id/btn_remplacer" />


</androidx.constraintlayout.widget.ConstraintLayout>


Gestion des clics de bouton et des transactions dans l'Activity :

public class FragmentActivity extends AppCompatActivity implements View.OnClickListener {


    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activite_fragment);

        findViewById(R.id.btn_ajouter).setOnClickListener(this);
        findViewById(R.id.btn_supprimer).setOnClickListener(this);
        findViewById(R.id.btn_remplacer).setOnClickListener(this);
    }


    @Override
    public void onClick(View v) {
        if (v.getId() == R.id.btn_ajouter) {
            // Ajout
            MonFragment monFragment = new MonFragment();
            // Dans l'Activity, gère l'ajout, la suppression, le remplacement, etc. des Fragment
            FragmentManager gestionnaireFragment = getSupportFragmentManager();
            // Commencer la transaction
            FragmentTransaction transactionFragment = gestionnaireFragment.beginTransaction();
            // Ajouter le Fragment et spécifier l'ID de mise en page associé et le tag
            transactionFragment.add(R.id.fcv, monFragment, "tag_monFragment");
            transactionFragment.commit();// Soumettre la transaction
        } else if (v.getId() == R.id.btn_supprimer) {
            // Suppression
            FragmentManager fm = getSupportFragmentManager();
            FragmentTransaction ft = fm.beginTransaction();

            // Trouver le Fragment par tag
            Fragment tagMonFragment = fm.findFragmentByTag("tag_monFragment");
            // Trouver le Fragment par un contrôle, retournera le Fragment actuellement associé
            Fragment fragment = fm.findFragmentById(R.id.fcv);
            // Supprimer le Fragment
            ft.remove(tagMonFragment);
            ft.commit();

        } else if (v.getId() == R.id.btn_remplacer) {
            // Remplacement
            FragmentManager fm = getSupportFragmentManager();
            FragmentTransaction ft = fm.beginTransaction();
            ft.replace(R.id.fcv, new FragmentBleu(), "tag_FragmentBleu");
            ft.commit();
        }
    }
}


Le FragmentBleu utilisé pour le remplacement :

public class FragmentBleu extends Fragment {
    private static final String TAG = "FragmentBleu";

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View vue = inflater.inflate(R.layout.layout_bleu, container, false);
        vue.findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getActivity(), "Je suis la mise en page bleue", Toast.LENGTH_SHORT).show();
            }
        });
        Log.i(TAG, "onCreateView: Association de la vue ici");
        return vue;
    }
}


Diagramme du flux d'opération :

Problèmes courants et recommandations :

  • add() superpose les Fragment ; si on ne veut pas de superposition, soit utiliser replace(), soit gérer l'état d'affichage avec hide/show.
  • Si on a besoin de "revenir en arrière avec le bouton Retour pour annuler les changements de Fragment", il faut appeler addToBackStack() dans la transaction (l'exemple de code ne l'inclut pas).
  • Les références fortement liées à la Vue dans le Fragment doivent être initialisées dans onViewCreated() et libérées dans onDestroyView() pour éviter les fuites de mémoire.

6.3 Points clés du cycle de vie des Fragment

Le cycle de vie des Fragment comprend deux aspects : le cycle de vie du Fragment lui-même et le cycle de vie de la Vue du Fragment. Lorsqu'un Fragment est placé dans la pile de retour ou détaché, il peut arriver que "le Fragment existe toujours, mais sa Vue est déjà détruite".

Par conséquent, il est recommandé d'initialiser les "références/écouteurs fortement liés à la Vue" dans onViewCreated() et de les libérer dans onDestroyView().

Le MonFragment du projet couvre déjà les rappels courants. Pour comprendre les journaux, on peut suivre cet ordre :

Exécutez les méthodes de cycle de vie suivantes de MonFragment :

  • onAttach : Le Fragment est associé à l'Activity
  • onCreate : Lorsque le Fragment est créé, utilisé pour initialiser les composants qui ne dépendent pas de la vue, par exemple obtenir des ressources ou définir des données.
  • onCreateView : Ici on associe la vue
  • onViewCreated : Lorsque la vue est créée, on peut initialiser les composants UI ou lier des données. La vue est entièrement créée à ce moment, on peut accéder aux éléments UI.
  • onActivityCreated : Après que la méthode onCreate() de l'Activity hôte a été exécutée, on peut interagir avec l'Activity ici ou dépendre de son contenu. À partir de l'API 28, cette méthode est obsolète, il est préférable d'utiliser onViewCreated().
  • onViewStateRestored : Après onCreateView() mais avant onStart(). En vérifiant si le paramètre savedInstanceState a des données, on peut restaurer l'état précédemment sauvegardé du Fragment, utilisé en paire avec onSaveInstanceState().
  • onStart : Lorsque le Fragment devient visible, la vue est sur le point d'être affichée à l'écran
  • onResume : Lorsque le Fragment peut interagir avec l'utilisateur, le Fragment est actif. Idéal pour reprendre des opérations en pause ou mettre à jour l'UI ici.
  • onPause : Lorsque le Fragment perd le focus utilisateur mais reste visible, on peut sauvegarder les données utilisateur ou mettre en pause les opérations coûteuses ici.
  • onSaveInstanceState : Avant que le Fragment ne soit détruit (par exemple, lorsque l'utilisateur tourne l'écran ou que le Fragment est mis en arrière-plan), appelé (généralement avant onPause() ou onStop()). Son rôle est de sauvegarder l'état instantané du Fragment (comme l'état UI ou des données temporaires) dans un Bundle, pour pouvoir le restaurer plus tard.
  • onStop : Lorsque le Fragment est complètement invisible. Idéal pour sauvegarder l'état ou libérer des ressources inutiles ici. On peut revenir à onStart() ou aller à onDestroyView()
  • onDestroyView : Lorsque les ressources associées à la vue du Fragment sont libérées. La vue est sur le point d'être détruite, idéal pour nettoyer les ressources liées à l'UI ici.
  • onDestroy : Lorsque le Fragment n'est plus nécessaire, généralement utilisé pour nettoyer les ressources globales. Idéal pour libérer des données persistantes, comme des variables membres, certaines ressources d'image ou d'objets, etc. Lorsque onDestroy() est déclenché, le cycle de vie de la vue du Fragment se termine
  • onDetach : Lorsque le Fragment est dissocié de l'Activity. Le cycle de vie du Fragment se termine
public class FragmentBleu extends Fragment {
    private static final String TAG = "FragmentBleu";

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View vue = inflater.inflate(R.layout.layout_bleu, container, false);
        vue.findViewById(R.id.tv).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Toast.makeText(getActivity(), "Je suis la mise en page bleue", Toast.LENGTH_SHORT).show();
            }
        });
        Log.i(TAG, "onCreateView: Association de la vue ici");
        return vue;
    }


    @Override
    public void onAttach(@NonNull Context context) {
        super.onAttach(context);
        // Appelé lorsque le Fragment est associé à l'Activity. On peut obtenir une référence à l'Activity ici
        // On peut directement getActivity() ici
        FragmentActivity activite = getActivity();
        Log.i(TAG, TAG + " onAttach: Le Fragment est associé à l'Activity");
    }

    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        // Appelé lorsque le Fragment est créé, utilisé pour initialiser les composants qui ne dépendent pas de la vue, par exemple obtenir des ressources ou définir des données.
        // Donc n'initialisez pas les éléments UI ici.

        Log.i(TAG, TAG + " onCreate: Le Fragment est créé");
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        // Appelé lorsque la vue est créée, on peut initialiser les composants UI ou lier des données. La vue est entièrement créée, on peut accéder aux éléments UI.
        Log.i(TAG, TAG + " onViewCreated: La vue est entièrement créée, on peut accéder aux éléments UI");
    }

    @Override
    public void onActivityCreated(@Nullable Bundle savedInstanceState) {
        super.onActivityCreated(savedInstanceState);
        // Appelé après que la méthode onCreate() de l'Activity hôte a été exécutée, on peut interagir avec l'Activity ici ou dépendre de son contenu.
        // À partir de l'API 28, cette méthode est obsolète, il est préférable d'utiliser onViewCreated().
        Log.i(TAG, TAG + " onActivityCreated: Méthode obsolète");
    }


    @Override
    public void onStart() {
        super.onStart();
        // Appelé lorsque le Fragment devient visible, la vue est sur le point d'être affichée à l'écran
        Log.i(TAG, TAG + " onStart: La vue est sur le point d'être affichée à l'écran");
    }

    @Override
    public void onResume() {
        super.onResume();
        // Appelé lorsque le Fragment peut interagir avec l'utilisateur, le Fragment est actif. Idéal pour reprendre des opérations en pause ou mettre à jour l'UI ici.
        Log.i(TAG, TAG + " onResume: Le Fragment peut interagir avec l'utilisateur");
    }

    @Override
    public void onPause() {
        super.onPause();
        // Appelé lorsque le Fragment perd le focus utilisateur mais reste visible, on peut sauvegarder les données utilisateur ou mettre en pause les opérations coûteuses ici.
        Log.i(TAG, TAG + " onPause: Le Fragment perd le focus utilisateur mais reste visible");
    }

    @Override
    public void onStop() {
        super.onStop();
        // Appelé lorsque le Fragment est complètement invisible. Idéal pour sauvegarder l'état ou libérer des ressources inutiles ici.
        Log.i(TAG, TAG + " onStop: Le Fragment est complètement invisible");
    }

    @Override
    public void onViewStateRestored(@Nullable Bundle savedInstanceState) {
        super.onViewStateRestored(savedInstanceState);
        // Appelé après onCreateView() mais avant onStart().
        // Il est utilisé pour restaurer l'état précédemment sauvegardé du Fragment, utilisé en paire avec onSaveInstanceState().
        Log.i(TAG, TAG + " onViewStateRestored: La vue est sur le point d'être affichée à l'écran");
    }

    @Override
    public void onSaveInstanceState(@NonNull Bundle outState) {
        super.onSaveInstanceState(outState);
        // Avant que le Fragment ne soit détruit (par exemple, lorsque l'utilisateur tourne l'écran ou que le Fragment est mis en arrière-plan), appelé (généralement avant onPause() ou onStop()).
        // Son rôle est de sauvegarder l'état instantané du Fragment (comme l'état UI ou des données temporaires) dans un Bundle, pour pouvoir le restaurer plus tard.
        Log.i(TAG, TAG + " onSaveInstanceState: On peut sauvegarder des données ici");
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        // Appelé lorsque les ressources associées à la vue du Fragment sont libérées. La vue est sur le point d'être détruite, idéal pour nettoyer les ressources liées à l'UI ici.
        Log.i(TAG, TAG + " onDestroyView: La vue est sur le point d'être détruite");
    }

    @Override
    public void onDestroy() {
        super.onDestroy();
        // Appelé lorsque le Fragment n'est plus nécessaire, généralement utilisé pour nettoyer les ressources globales. Idéal pour libérer des données persistantes, comme des variables membres, certaines ressources d'image ou d'objets, etc.
        // Lorsque onDestroy() est déclenché, le cycle de vie de la vue du Fragment se termine
        Log.i(TAG, TAG + " onDestroy: Détruit, le cycle de vie de la vue du Fragment est terminé");
    }

    @Override
    public void onDetach() {
        super.onDetach();
        // Appelé lorsque le Fragment est dissocié de l'Activity. Le cycle de vie du Fragment se termine
        Log.i(TAG, TAG + " onDetach: Le Fragment est dissocié de l'Activity");
    }

}


7. Communication entre Activity et Fragment

La communication entre Activity et Fragment est de deux types principaux :

  • Activity → Fragment : passage de valeurs lors de la création via arguments ; passage de valeurs après l'association via des méthodes set
  • Fragment → Activity : interface de rappel (ou ViewModel partagé, cet article se concentre sur l'interface de rappel)

7.1 Passage de valeurs de l'Activity vers le Fragment

Exemple : l'Activity transmet un numéro de téléphone au Fragment pour une recherche. Code côté Activity :

public class ActiviteModificationInfoUtilisateur extends AppCompatActivity implements ReturnInfoFragment.OnFragmentCallback {

    private EditText etMobile;
    private ReturnInfoFragment fragment;
    private TextView tvInfo;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activite_modification_info_utilisateur);


        fragment = new ReturnInfoFragment();
      
        // Passage de valeurs lors de l'instanciation du Fragment
        Bundle bundle = new Bundle();
        bundle.putLong("cle_mobile", 13811138111l);
        fragment.setArguments(bundle);
      
        // Définition du callback du Fragment
        fragment.setCallback(this);
      
        FragmentTransaction ft = getSupportFragmentManager().beginTransaction();
        ft.add(R.id.fcv, fragment, "mon_ReturnInfoFragment");
        ft.commit();

        tvInfo = findViewById(R.id.tv_info);
        etMobile = findViewById(R.id.et_pseudo);

        findViewById(R.id.envoyer).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                String string = etMobile.getText().toString();
                if (string != null) {
                    fragment.setMobile(Long.valueOf(string));
                }
            }
        });
    }

    @Override
    public void onQuery(String nom, int age, long mobile) {
        tvInfo.setText("Informations utilisateur trouvées :\nNom : " + nom + "\nÂge : " + age + "\nMobile : " + mobile);
    }

    @Override
    public void onResultNotFound(String resultat) {
        Toast.makeText(this, resultat, Toast.LENGTH_SHORT).show();
    }
}


Mise en page :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/principal"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical"
    android:padding="16dp"
    tools:context=".activite.utilisateur.ModificationInfoUtilisateurActivite">

    <EditText
        android:id="@+id/et_pseudo"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="Saisir le numéro de mobile de l'utilisateur à rechercher"
        android:inputType="phone" />

    <Button
        android:id="@+id/envoyer"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="Envoyer vers" />

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/fcv"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp" />

    <TextView
        android:id="@+id/tv_recherche"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp"
        android:text="Informations retournées par le Fragment :" />

    <TextView
        android:id="@+id/tv_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="Informations utilisateur trouvées :\nNom :\nÂge :\nMobile :" />

</LinearLayout>


Cela inclut deux méthodes de passage de valeurs :

  1. Passage de valeurs lors de l'instanciation du Fragment : via Bundle + setArguments(), récupéré dans onCreate() du Fragment avec getArguments().
  2. Passage de valeurs après que le Fragment est associé à l'Activity : définir une méthode setMobile(long mobile), que l'Activity appelle directement pour mettre à jour l'état interne du Fragment.

7.2 Retour de résultats du Fragment vers l'Activity

Côté Fragment, les résultats sont renvoyés à l'Activity via une interface de rappel :

public class ReturnInfoFragment extends Fragment {

    private static final String TAG = "ReturnInfoFragment";
    private long mobile;
    private TextView tvQuery;

    private OnFragmentCallback callback;

    /**
     * Accepter les paramètres de l'Activity dans onCreate
     *
     * @param savedInstanceState
     */
    @Override
    public void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        Bundle bundle = getArguments();
        mobile = bundle.getLong("cle_mobile");
    }

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
        View vue = inflater.inflate(R.layout.layout_retour_info, container, false);
        return vue;
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);
        tvQuery = view.findViewById(R.id.tv_query);
        tvQuery.setText("Numéro de téléphone en cours de recherche : " + mobile);

        view.findViewById(R.id.btn_retour).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                if (mobile == 13800138000l) {
                    // Informations utilisateur trouvées
                    callback.onQuery("Jean", 31, mobile);
                } else {
                    // Aucune information trouvée
                    callback.onResultNotFound("Aucun utilisateur trouvé !");
                }
            }
        });

        view.findViewById(R.id.btn_sauter).setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {

                Intent intent = new Intent(getActivity(), SecondeActivite.class);
                startActivity(intent);

            }
        });

    }

    public void setCallback(OnFragmentCallback callback) {
        this.callback = callback;
    }

    /**
     * Si le Fragment est déjà instancié et associé à l'Activity,
     * et que l'Activity veut transmettre des valeurs au Fragment,
     * il suffit de définir une méthode set
     *
     * @param mobile
     */
    public void setMobile(long mobile) {
        this.mobile = mobile;
        Log.i(TAG, "setMobile: mobile = " + mobile);
        if (mobile != 0) {
            tvQuery.setText("Numéro de téléphone en cours de recherche : " + mobile);
        }
    }

    public interface OnFragmentCallback {

        /**
         * Informations utilisateur trouvées
         *
         * @param nom
         * @param age
         * @param mobile
         */
        void onQuery(String nom, int age, long mobile);

        /**
         * Aucune information trouvée
         *
         * @param resultat Retourne des informations d'erreur
         */
        void onResultNotFound(String resultat);

    }
}


Mise en page du Fragment :

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:orientation="vertical"
    android:background="#DCDCDC"
    android:padding="16dp">

    <TextView
        android:id="@+id/tv_query"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="Numéro de téléphone en cours de recherche : xxx" />

    <TextView
        android:id="@+id/tv_info"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="30dp"
        android:text="Informations utilisateur trouvées :\nNom :\nÂge :\nMobile :" />


    <Button
        android:id="@+id/btn_retour"
        android:layout_width="match_parent"
        android:layout_marginTop="10dp"
        android:layout_height="wrap_content"
        android:text="Retourner les informations de recherche à l'Activity" />

    <Button
        android:id="@+id/btn_sauter"
        android:layout_width="match_parent"
        android:layout_marginTop="10dp"
        android:layout_height="wrap_content"
        android:text="Sauter vers seconde" />


</LinearLayout>


Flux d'interaction :

Problèmes courants :

  • Avant d'utiliser le rappel dans le Fragment, il faut s'assurer que callback est défini ; si le cycle de vie du Fragment précède la définition du rappel, un pointeur nul peut se produire.
  • Avant de mettre à jour directement l'UI dans le Fragment (comme tvQuery.setText), il faut s'assurer que la Vue est déjà créée (dans l'exemple, tvQuery est initialisé dans onViewCreated).

Conclusion

En examinant ensemble le "cycle de vie, changements de configuration, sauvegarde d'état, règles de pile de tâches et flux d'autorisations" des Activity/Fragment, on peut plus facilement expliquer de nombreux problèmes apparents :

  • Perte de données après rotation de l'écran : en réalité, c'est la reconstruction de l'Activity, nécessitant la sauvegarde/restauration d'état ou l'utilisation de méthodes plus robustes de conservation des données.
  • Comportement anormal du bouton Retour : généralement lié à la combinaison de la pile de tâches, des modes de lancement et des drapeaux Intent.
  • Pointeurs nuls dans Fragment ou interface non mise à jour : la plupart du temps, le cycle de vie de la Vue n'est pas aligné avec celui du Fragment, ou le moment d'initialisation du rappel/référence n'est pas correct.

Une fois ces règles formalisées en modèles réutilisables, même les pages d'applications complexes peuvent maintenir un comportement prévisible et maintenable.

Étiquettes: Android Activity Fragment Cycle de vie Configuration

Publié le 19 juillet à 05h50