Concepts et implémentation de Jetpack Compose
Les composants tenant compte des cycles de vie effectuent des actions en réponse à une modification de l'état du cycle de vie 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. This
peut entraîner une condition de concurrence où la méthode onStop() se termine avant le
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.
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
Lifecycle classe. 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.
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'
LifecycleOwner interface, qui est expliquée dans la section suivante.
LifecycleOwner
LifecycleOwner est une interface à méthode unique qui indique que la classe
possède un Lifecycle. Elle comporte 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 MyLocationListener classe
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 Lifecycle classe 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'LifecycleOwner interface.
Si vous souhaitez créer une interface LifecycleOwner,
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
ViewModelet observez unLiveDataobjet pour refléter 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
ViewModelclasse.ViewModeldoit servir de connecteur entre votre contrôleur d'interface utilisateur et le reste de votre application. Soyez prudent, carViewModel's n'est pas responsable de la récupération de données (à partir d'un réseau, par exemple). Au lieu de cela,ViewModeldoit 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
ViewouActivitycontexte dans votreViewModel. SiViewModelsurvit à 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 à un AppCompatActivity ou
à un Fragment, l'état du Lifecycle's passe à CREATED et
l'événement ON_STOP est déclenché lorsque la méthode onSaveInstanceState() du AppCompatActivity ou
du Fragment's est appelée.
Lorsqu'un Fragment ou l'état de la AppCompatActivity's est enregistré à l'aide de
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() pour
détails.
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 AppCompatActivity's onStop() 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écessairementonStop. 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
LiveDataclasse doit implémenter la solution de secours fournie parLifecycleversionbeta 2et 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
- Sunflower, une application de démonstration qui montre les bonnes pratiques associées aux composants d'architecture