Enregistrement des états dans les applications Android

Aujourd'hui, je voulais partager avec vous une autre approche pour maintenir l'état lors du développement d'applications Android. Ce n'est pas un secret que notre application en arrière-plan peut être supprimée à tout moment et ce problème devient plus urgent avec l'introduction d'économies d'énergie agressives - bonjour Oreo . De plus, personne n'a annulé le changement de configuration sur le téléphone: orientation, changement de langue, etc. Et pour ouvrir l'application depuis l'arrière-plan et afficher l'interface dans le dernier état, nous devons prendre soin de la sauvegarder. Oh, ce onSaveInstanceState .

onSaveInstanceState

Quelle douleur il nous a apportée.

Je donnerai des exemples plus tard en utilisant Clean Achitecture et Dagger2 , alors soyez prêt pour cela :)

Le problème du maintien de l'état en fonction des tâches peut être résolu de plusieurs manières:

  1. Enregistrez les données primaires dans l'état onSaveInstanceState de l' hôte (activité, fragment), telles que l'identifiant de la page, de l'utilisateur, etc. Ce dont nous avons besoin pour l'acquisition initiale des données et l'affichage des pages.
  2. Enregistrez les données reçues dans le programme interactif dans le référentiel (SharedPreference, Database.
  3. Utilisez des fragments rethein pour enregistrer et restaurer les données lors de la recréation d'activité.

Mais que se passe-t-il si nous devons restaurer l'état de l'interface utilisateur, ainsi que la réaction actuelle de l'interface à l'action de l'utilisateur? Pour simplifier, considérons la solution à ce problème en utilisant un exemple réel. Nous avons une page de connexion - l'utilisateur entre ses données, clique sur le bouton puis reçoit un appel entrant. Notre application passe en arrière-plan. Le système le tue. Ça fait peur, non?)

L'utilisateur revient sur l'application et que doit-il voir? Au minimum, poursuivre l'opération de connexion et afficher les progrès. Si l'application a réussi à se connecter avant d'appeler la méthode onDestroy de l'hôte, l'utilisateur verra la navigation vers l'écran de démarrage de l'application. Ce comportement peut être facilement résolu à l'aide de la machine d'état. Très bon rapport de Yandex . Dans le même article, je vais essayer de partager mes réflexions mâchées sur ce rapport.

Maintenant un petit code:

État de base

public interface BaseState<VIEW extends BaseView, OWNER extends BaseOwner> extends Parcelable{ /** * Get name * * @return name */ @NonNull String getName(); /** * Enter to state * * @param aView view */ void onEnter(@NonNull VIEW aView); /** * Exit from state */ void onExit(); /** * Return to next state */ void forward(); /** * Return to previous state */ void back(); /** * Invalidate view * * @param aView view */ void invalidateView(@NonNull VIEW aView); /** * Get owner * * @return owner */ @NonNull OWNER getOwner(); /** * Set owner * * @param aOwner owner */ void setOwner(@NonNull OWNER aOwner); } 

Gouverneur de base

 public interface BaseOwner<VIEW extends BaseView, STATE extends BaseState> extends BasePresenter<VIEW>{ /** * Set state * * @param aState state */ void setState(@NonNull STATE aState); } 

BasestateImpl

 public abstract class BaseStateImpl<VIEW extends BaseView, OWNER extends BaseOwner> implements BaseState<VIEW, OWNER>{ private OWNER mOwner; @NonNull @Override public String getName(){ return getClass().getName(); } @Override public void onEnter(@NonNull final VIEW aView){ Timber.d( getName()+" onEnter"); //depend from realization } @Override public void onExit(){ Timber.d(getName()+" onExit"); //depend from realization } @Override public void forward(){ Timber.d(getName()+" forward"); onExit(); //depend from realization } @Override public void back(){ Timber.d(getName()+" back"); onExit(); //depend from realization } @Override public void invalidateView(@NonNull final VIEW aView){ Timber.d(getName()+" invalidateView"); //depend from realization } @NonNull @Override public OWNER getOwner(){ return mOwner; } @Override public void setOwner(@NonNull final OWNER aOwner){ mOwner = aOwner; } 

Dans notre cas, le propriétaire de l'État sera un présentateur.

En regardant la page de connexion, trois états uniques peuvent être distingués:

LoginInitState , LoginProgressingState , LoginCompleteState .

Alors, réfléchissez maintenant à ce qui se passe dans ces États.

LoginInitState nous avons la validation des champs et en cas de validation réussie, le bouton de connexion devient actif.

Une demande de connexion est effectuée dans LoginProgressingState , un jeton est enregistré, des demandes supplémentaires sont effectuées pour démarrer l'activité principale de l'application.

LoginCompleteState navigue vers l'écran principal de l'application.

Conditionnellement, la transition entre les états peut être affichée dans le diagramme suivant:

Diagramme d'état de connexion

L'état LoginProgressingState se ferme si l'opération de connexion réussit dans l'état LoginCompleteState et si LoginInitState échoue . Ainsi, lorsque notre vue se détache, nous avons un état complètement déterministe du présentateur. Nous devons enregistrer cet état en utilisant le mécanisme Android standard onSaveInstanceState . Pour ce faire, tous les états de connexion doivent implémenter l'interface Parcelable . Par conséquent, nous élargissons notre interface de base BaseState .

Ensuite, nous avons une question, comment transmettre cet état du présentateur à notre hôte? La façon la plus simple est de demander au présentateur des données de l'hôte, mais du point de vue de l'architecture, cela ne semble pas très bon. Et ainsi retenir des fragments venus à notre aide. Nous pouvons créer une interface pour le cache et l'implémenter dans ce fragment:

 public interface Cache{ /** * Save cache data * * @param aData data */ void saveCacheData(@Nullable Parcelable aData); @Nullable Parcelable getCacheData(); /** * Check that cache exist * * @return true if cache exist */ boolean isCacheExist(); } 

Ensuite, nous injectons le fragment de cache dans le constructeur de l'interacteur, comme Cache. Nous ajoutons des méthodes dans l'intégrateur pour obtenir et enregistrer l'état dans le cache. Maintenant, à chaque changement de l'état du présentateur, nous pouvons enregistrer l'état dans l'interaction, et l'interaction à son tour enregistre dans le cache. Tout devient très logique. Lorsque l'hôte est chargé pour la première fois, le présentateur reçoit l'état de l'interacteur, qui à son tour reçoit les données du cache. Voici à quoi ressemble la méthode de changement d'état dans le présentateur:

 @Override public void setState(@NonNull final LoginBaseState aState){ mState.onExit(); mState = aState; clearDisposables(); mState.setOwner(this); mState.onEnter(getView()); mInteractor.setState(mState); } 

Je voudrais noter ce point - la sauvegarde des données via le cache peut être effectuée pour toutes les données, pas seulement pour l'état. Vous devrez peut-être créer votre propre cache d'extraits de code pour stocker les données actuelles. Cet article décrit une approche générale. Je voudrais également noter que la situation en question est très exagérée. Dans la vie, il faut résoudre des problèmes beaucoup plus compliqués. Par exemple, dans notre application, trois pages ont été combinées: connexion, enregistrement, récupération de mot de passe. Dans ce cas, le diagramme d'état était le suivant:

Diagramme d'état dans un projet réel

Par conséquent, en utilisant le modèle d'état et l'approche décrits dans l'article, nous avons pu rendre le code plus lisible et maintenable. Et ce qui est important, c'est de restaurer l'état actuel de l'application.

Le code complet peut être consulté dans le référentiel .

Source: https://habr.com/ru/post/fr420023/


All Articles