Gérer les cycles de vie à l'aide de composants tenant compte des cycles de vie   Fait partie d'Android Jetpack.

Les composants tenant compte des cycles de vie effectuent des actions en réponse à une modification de l'état d'un autre composant (par exemple, des activités ou des fragments). Ces composants vous aident à produire un code mieux organisé, souvent plus léger, et plus facile à gérer.

Un modèle courant consiste à implémenter les actions des composants dépendants dans les méthodes de cycle de vie des activités et des fragments. Toutefois, ce modèle conduit à une mauvaise organisation du code et à la prolifération des erreurs. En utilisant des composants tenant compte des cycles de vie, vous pouvez déplacer le code des composants dépendants des méthodes du cycle de vie vers les composants eux-mêmes.

Le package androidx.lifecycle fournit des classes et des interfaces qui vous permettent de créer des composants tenant compte des cycles de vie. Ces composants peuvent ajuster automatiquement leur comportement en fonction de l'état actuel du cycle de vie d'une activité ou d'un fragment.

La plupart des composants d'application définis dans le framework Android sont associés à des cycles de vie. Les cycles de vie sont gérés par le système d'exploitation ou le code du framework qui s'exécute dans votre processus. Ils sont essentiels au fonctionnement d'Android, et votre application doit les respecter. Sinon, vous risquez de provoquer des fuites de mémoire ou même des plantages de l'application.

Imaginez que nous ayons une activité qui indique la position de l'appareil à l'écran. Une implémentation courante peut se présenter comme suit :

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val callback: (Location) -> Unit
) {

    fun start() {
        // connect to system location service
    }

    fun stop() {
        // disconnect from system location service
    }
}

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        myLocationListener.start()
        // manage other components that need to respond
        // to the activity lifecycle
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Java

class MyLocationListener {
    public MyLocationListener(Context context, Callback callback) {
        // ...
    }

    void start() {
        // connect to system location service
    }

    void stop() {
        // disconnect from system location service
    }
}

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    @Override
    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, (location) -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        myLocationListener.start();
        // manage other components that need to respond
        // to the activity lifecycle
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
        // manage other components that need to respond
        // to the activity lifecycle
    }
}

Bien que cet exemple semble correct, dans une application réelle, vous avez trop d'appels gérant l'interface utilisateur et d'autres composants en réponse à l'état actuel du cycle de vie. La gestion de plusieurs composants place une quantité considérable de code dans les méthodes de cycle de vie, telles que onStart() et onStop(), ce qui les rend difficiles à gérer.

De plus, rien ne garantit que le composant démarrera avant l'arrêt de l'activité ou du fragment. Cela est particulièrement vrai si nous devons effectuer une opération de longue durée telle qu'une vérification de la configuration dans onStart(). Cela peut entraîner une condition de concurrence où la méthode onStop() se termine avant onStart(), ce qui maintient le composant actif plus longtemps que nécessaire.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this) { location ->
            // update UI
        }
    }

    public override fun onStart() {
        super.onStart()
        Util.checkUserStatus { result ->
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start()
            }
        }
    }

    public override fun onStop() {
        super.onStop()
        myLocationListener.stop()
    }

}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, location -> {
            // update UI
        });
    }

    @Override
    public void onStart() {
        super.onStart();
        Util.checkUserStatus(result -> {
            // what if this callback is invoked AFTER activity is stopped?
            if (result) {
                myLocationListener.start();
            }
        });
    }

    @Override
    public void onStop() {
        super.onStop();
        myLocationListener.stop();
    }
}

Le package androidx.lifecycle fournit des classes et des interfaces qui vous aident à résoudre ces problèmes de manière résiliente et isolée.

Lifecycle (Cycle de vie)

Lifecycle est une classe qui contient les informations sur l'état de cycle de vie d'un composant (comme une activité ou un fragment) et permet à d'autres objets d'observer cet état.

Lifecycle utilise deux énumérations principales pour suivre l'état du cycle de vie du composant associé :

Événement
Les événements de cycle de vie qui sont déclenchés à partir du framework et de la classe Lifecycle. Ces événements sont mappés aux événements de rappel dans les activités et les fragments.
État
État actuel du composant suivi par l'objet Lifecycle.
Schéma des états du cycle de vie
Figure 1. États et événements qui composent le cycle de vie de l'activité Android

Considérez les états comme les nœuds d'un graphe et les événements comme les arêtes entre ces nœuds.

Une classe peut surveiller l'état du cycle de vie du composant en implémentant DefaultLifecycleObserver et en forçant les méthodes correspondantes telles que onCreate, onStart, etc. Vous pouvez ensuite ajouter un observateur en appelant la méthode addObserver() de la classe Lifecycle et en transmettant une instance de votre observateur, comme illustré dans l'exemple suivant :

Kotlin

class MyObserver : DefaultLifecycleObserver {
    override fun onResume(owner: LifecycleOwner) {
        connect()
    }

    override fun onPause(owner: LifecycleOwner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(MyObserver())

Java

public class MyObserver implements DefaultLifecycleObserver {
    @Override
    public void onResume(LifecycleOwner owner) {
        connect()
    }

    @Override
    public void onPause(LifecycleOwner owner) {
        disconnect()
    }
}

myLifecycleOwner.getLifecycle().addObserver(new MyObserver());

Dans l'exemple ci-dessus, l'objet myLifecycleOwner implémente l'interface LifecycleOwner, comme expliqué dans la section suivante.

LifecycleOwner

LifecycleOwner est une interface à méthode unique qui indique que la classe possède un Lifecycle. Cette interface possède une méthode, getLifecycle(), qui doit être implémentée par la classe. Si vous essayez de gérer le cycle de vie de l'ensemble d'un processus d'application, consultez ProcessLifecycleOwner.

Cette interface élimine la propriété d'un objet Lifecycle de classes individuelles, telles que Fragment et AppCompatActivity, et autorise l'écriture de composants compatibles. Toute classe d'application personnalisée peut implémenter l'interface LifecycleOwner.

Les composants qui implémentent DefaultLifecycleObserver fonctionnent de manière fluide avec ceux qui implémentent LifecycleOwner, car un propriétaire peut fournir un cycle de vie qui peut être enregistré par un observateur pour observation.

Pour l'exemple de suivi de la position, nous pouvons faire en sorte que la classe MyLocationListener implémente DefaultLifecycleObserver, puis l'initialiser avec le paramètre Lifecycle de l'activité dans la méthode onCreate(). Cela permet à la classe MyLocationListener d'être autonome, c'est-à-dire que la logique de réaction aux changements d'état du cycle de vie est déclarée dans MyLocationListener plutôt que dans l'activité. Le fait que les composants individuels stockent leur propre logique facilite la gestion de la logique des activités et des fragments.

Kotlin

class MyActivity : AppCompatActivity() {
    private lateinit var myLocationListener: MyLocationListener

    override fun onCreate(...) {
        myLocationListener = MyLocationListener(this, lifecycle) { location ->
            // update UI
        }
        Util.checkUserStatus { result ->
            if (result) {
                myLocationListener.enable()
            }
        }
    }
}

Java

class MyActivity extends AppCompatActivity {
    private MyLocationListener myLocationListener;

    public void onCreate(...) {
        myLocationListener = new MyLocationListener(this, getLifecycle(), location -> {
            // update UI
        });
        Util.checkUserStatus(result -> {
            if (result) {
                myLocationListener.enable();
            }
        });
  }
}

Un cas d'utilisation courant consiste à éviter d'effectuer certains rappels si Lifecycle n'est pas dans un état favorable. Par exemple, si le rappel exécute une transaction de fragment après l'enregistrement de l'état de l'activité, cela déclenche un plantage. Par conséquent, ce rappel est toujours à éviter.

Pour faciliter ce cas d'utilisation, la classe Lifecycle permet à d'autres objets d'interroger l'état actuel.

Kotlin

internal class MyLocationListener(
        private val context: Context,
        private val lifecycle: Lifecycle,
        private val callback: (Location) -> Unit
): DefaultLifecycleObserver {

    private var enabled = false

    override fun onStart(owner: LifecycleOwner) {
        if (enabled) {
            // connect
        }
    }

    fun enable() {
        enabled = true
        if (lifecycle.currentState.isAtLeast(Lifecycle.State.STARTED)) {
            // connect if not connected
        }
    }

    override fun onStop(owner: LifecycleOwner) {
        // disconnect if connected
    }
}

Java

class MyLocationListener implements DefaultLifecycleObserver {
    private boolean enabled = false;
    public MyLocationListener(Context context, Lifecycle lifecycle, Callback callback) {
       ...
    }

    @Override
    public void onStart(LifecycleOwner owner) {
        if (enabled) {
           // connect
        }
    }

    public void enable() {
        enabled = true;
        if (lifecycle.getCurrentState().isAtLeast(STARTED)) {
            // connect if not connected
        }
    }

    @Override
    public void onStop(LifecycleOwner owner) {
        // disconnect if connected
    }
}

Avec cette implémentation, notre classe LocationListener tient entièrement compte des cycles de vie. Si vous devez utiliser notre LocationListener à partir d'une autre activité ou d'un autre fragment, il vous suffit de l'initialiser. Toutes les opérations de configuration et de suppression sont gérées par la classe elle-même.

Si une bibliothèque fournit des classes qui doivent fonctionner avec le cycle de vie Android, nous vous recommandons d'utiliser des composants qui tiennent compte des cycles de vie. Les bibliothèques clientes peuvent facilement intégrer ces composants sans gestion manuelle du cycle de vie côté client.

Implémenter un LifecycleOwner personnalisé

Les fragments et les activités de la bibliothèque Support 26.1.0 et versions ultérieures implémentent déjà l'interface LifecycleOwner.

Si vous souhaitez créer une interface LifecycleOwner à partir d'une classe personnalisée, vous pouvez utiliser la classe LifecycleRegistry, mais vous devez transférer les événements dans cette classe, comme illustré dans l'exemple de code suivant :

Kotlin

class MyActivity : Activity(), LifecycleOwner {

    private lateinit var lifecycleRegistry: LifecycleRegistry

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleRegistry = LifecycleRegistry(this)
        lifecycleRegistry.markState(Lifecycle.State.CREATED)
    }

    public override fun onStart() {
        super.onStart()
        lifecycleRegistry.markState(Lifecycle.State.STARTED)
    }

    override fun getLifecycle(): Lifecycle {
        return lifecycleRegistry
    }
}

Java

public class MyActivity extends Activity implements LifecycleOwner {
    private LifecycleRegistry lifecycleRegistry;

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

        lifecycleRegistry = new LifecycleRegistry(this);
        lifecycleRegistry.markState(Lifecycle.State.CREATED);
    }

    @Override
    public void onStart() {
        super.onStart();
        lifecycleRegistry.markState(Lifecycle.State.STARTED);
    }

    @NonNull
    @Override
    public Lifecycle getLifecycle() {
        return lifecycleRegistry;
    }
}

Bonnes pratiques concernant les composants tenant compte des cycles de vie

  • Faites en sorte que vos contrôleurs d'interface utilisateur (activités et fragments) soient aussi légers que possible. Ils ne doivent pas essayer d'acquérir leurs propres données. Pour ce faire, utilisez un objet ViewModel et observez un objet LiveData qui reflète les modifications apportées sur les vues.
  • Essayez de créer des interfaces utilisateur basées sur les données pour lesquelles votre contrôleur d'interface utilisateur est chargé de mettre à jour les vues à mesure que les données changent, ou de transmettre les actions des utilisateurs au ViewModel.
  • Placez votre logique de données dans la classe ViewModel. ViewModel doit servir de connecteur entre votre contrôleur d'interface utilisateur et le reste de votre application. Soyez prudent, car ViewModel n'est pas responsable de la récupération de données (à partir d'un réseau, par exemple). Au lieu de cela, ViewModel doit appeler le composant approprié pour récupérer les données, puis renvoyer le résultat au contrôleur d'interface utilisateur.
  • Utilisez la liaison de données pour maintenir une interface propre entre vos vues et le contrôleur d'interface utilisateur. Cela vous permet de rendre vos vues plus déclaratives et de réduire le code de mise à jour que vous devez écrire dans vos activités et fragments. Si vous préférez effectuer cette opération dans le langage de programmation Java, utilisez une bibliothèque telle que Butter Knife, qui permet d'éviter l'utilisation de code récurrent et d'obtenir une meilleure abstraction.
  • Si votre interface utilisateur est complexe, envisagez de créer une classe presenter pour gérer les modifications de l'interface utilisateur. Cette tâche peut être laborieuse, mais elle peut faciliter le test des composants de votre interface utilisateur.
  • Évitez de référencer un contexte View ou Activity dans votre ViewModel. Si ViewModel survit à l'activité (en cas de modification de la configuration), votre activité fuit et n'est pas correctement éliminée par le récupérateur de mémoire.
  • Utilisez les coroutines Kotlin pour gérer les tâches de longue durée et les autres opérations pouvant s'exécuter de manière asynchrone.

Cas d'utilisation des composants tenant compte des cycles de vie

Les composants tenant compte des cycles de vie peuvent faciliter considérablement la gestion des cycles de vie dans de nombreux cas. Voici quelques exemples :

  • Alterner entre des mises à jour de position plus ou moins précises. Utilisez des composants tenant compte du cycle de vie pour obtenir des mises à jour précises de la position lorsque votre application de localisation est visible et des notifications moins précises lorsque l'application est en arrière-plan. LiveData, un composant qui tient compte des cycles de vie, permet à votre application d'actualiser automatiquement l'interface utilisateur lorsque l'utilisateur change de position.
  • Arrêt et démarrage de la mise en mémoire tampon de vidéos. Utilisez des composants qui tiennent compte des cycles de vie pour lancer la mise en mémoire tampon de vidéos dès que possible, mais différez la lecture jusqu'à ce que l'application soit complètement démarrée. Vous pouvez également utiliser des composants qui tiennent compte des cycles de vie pour arrêter la mise en mémoire tampon lorsque votre application est détruite.
  • Démarrage et arrêt de la connectivité réseau. Utilisez des composants qui tiennent compte des cycles de vie pour permettre la mise à jour en direct (streaming) des données réseau lorsqu'une application est au premier plan, et également pour la mettre en pause automatiquement lorsque l'application passe en arrière-plan.
  • Mise en pause et réactivation des drawables animés. Utilisez des composants qui tiennent compte des cycles de vie pour gérer la mise en pause des drawables lorsque l'application est en arrière-plan, et reprendre les drawables une fois qu'elle passe au premier plan.

Gérer les événements d'arrêt

Lorsqu'un Lifecycle appartient à une AppCompatActivity ou à un Fragment, l'état du Lifecycle passe à CREATED et l'événement ON_STOP est déclenché lorsque la onSaveInstanceState() de la AppCompatActivity ou du Fragment est appelée.

Lorsqu'un Fragment ou l'état de la AppCompatActivity est enregistré via onSaveInstanceState(), son interface utilisateur est considérée comme immuable jusqu'à ce que ON_START soit appelé. Toute modification de l'interface utilisateur après l'enregistrement de l'état risque de provoquer des incohérences dans l'état de navigation de votre application. C'est pourquoi FragmentManager génère une exception si l'application exécute une FragmentTransaction une fois l'état enregistré. Pour en savoir plus, consultez commit().

LiveData permet d'éviter ce cas de figure en s'abstenant d'appeler son observateur si le Lifecycle associé à celui-ci n'est pas au moins STARTED. En arrière-plan, il appelle isAtLeast() avant de décider d'appeler son observateur.

Malheureusement, la méthode onStop() de AppCompatActivity est appelée après onSaveInstanceState(), ce qui laisse un intervalle de temps au cours duquel les changements d'état de l'interface utilisateur ne sont pas autorisés, alors que le Lifecycle n'a pas encore été défini sur l'état CREATED.

Pour éviter ce problème, la classe Lifecycle de la version beta2 et des versions inférieures marque l'état comme CREATED sans déclencher l'événement. Ainsi, tout code qui vérifie l'état actuel obtient la valeur réelle, même si l'événement n'est pas déclenché tant que onStop() n'a pas été appelé par le système.

Malheureusement, cette solution présente deux problèmes majeurs :

  • Au niveau d'API 23 et inférieur, le système Android enregistre l'état d'une activité, même si elle est partiellement couverte par une autre activité. En d'autres termes, le système Android appelle onSaveInstanceState(), mais pas nécessairement onStop(). Cela crée un intervalle potentiellement long pendant lequel l'observateur pense que le cycle de vie est actif, même si l'état de son interface utilisateur ne peut pas être modifié.
  • Toute classe qui souhaite exposer un comportement semblable à la classe LiveData doit implémenter la solution de secours fournie par Lifecycle version beta 2 et inférieure.

Ressources supplémentaires

Pour en savoir plus sur la gestion des cycles de vie avec des composants qui tiennent compte des cycles de vie, consultez les ressources supplémentaires suivantes.

Exemples

Ateliers de programmation

Blogs