Enregistrer les états de l'interface utilisateur

Ce guide porte sur les attentes des utilisateurs concernant l'état de l'interface utilisateur et sur les options disponibles pour conserver l'état.

Pour assurer une bonne expérience utilisateur, il est essentiel d'enregistrer et de restaurer l'état d'interface utilisateur d'une activité rapidement après la destruction des activités ou des applications par le système. Bien que les utilisateurs s'attendent à ce que l'état de l'interface utilisateur reste le même, le système peut détruire l'activité et son état stocké.

Pour aligner les attentes des utilisateurs avec le comportement du système, vous pouvez combiner les méthodes suivantes :

La solution optimale dépend de la complexité de vos données d'interface utilisateur, des cas d'utilisation de votre application, ainsi que du bon équilibre entre la vitesse d'accès aux données et l'utilisation de la mémoire.

Assurez-vous que votre application répond aux attentes des utilisateurs et propose une interface rapide et réactive. Évitez les retards de chargement des données dans l'interface utilisateur, en particulier après les modifications de configuration courantes telles que la rotation.

Attentes des utilisateurs et comportement du système

En fonction de l'action effectuée par un utilisateur, il s'attend à ce que l'état d'une activité soit effacé ou préservé. Dans certains cas, le système effectue automatiquement ce à quoi l'utilisateur s'attend. Dans d'autres cas, le système fait l'inverse de ce à quoi l'utilisateur s'attend.

Fermeture de l'état de l'UI déclenchée par l'utilisateur

Au démarrage d'une activité, l'utilisateur s'attend à ce que l'état temporaire de cette activité reste le même jusqu'à ce qu'il la ferme complètement. L'utilisateur peut ignorer complètement une activité en :

  • balayant l'activité hors de l'écran "Overview (Recents)" [Présentation (récents)] ;
  • fermant l'application ou en forçant l'arrêt à partir de l'écran "Settings" (Paramètres) ;
  • redémarrant l'appareil ;
  • effectuant une action de "finition" (reposant sur Activity.finish()).

Dans un cas de fermeture complète, l'utilisateur pense avoir définitivement quitté l'activité et s'attend à ce qu'elle soit réinitialisée. Le comportement du système en cas de fermeture correspond aux attentes de l'utilisateur : l'instance d'activité est détruite et supprimée de la mémoire, ainsi que tout état stocké dans celui-ci et toute sauvegarde d'instance d'état enregistrée associée à l'activité.

Plusieurs exceptions à cette règle concernant la fermeture complète existent. Un utilisateur peut par exemple s'attendre à ce qu'un navigateur le redirige vers la page qu'il consultait avant de quitter le navigateur à l'aide du bouton "Retour".

Fermeture de l'état de l'UI déclenchée par le système

Un utilisateur s'attend à ce que l'état de l'interface utilisateur d'une activité reste le même en cas de modification de la configuration, par exemple en cas de rotation ou d'un passage en mode multifenêtre. Toutefois, le système détruit par défaut l'activité lorsqu'une telle modification de configuration se produit, effaçant tout état d'interface utilisateur stocké dans l'instance d'activité. Pour en savoir plus sur les configurations d'appareil, consultez la page de référence concernant la configuration. Notez qu'il est possible (mais pas recommandé) d'ignorer le comportement par défaut en cas de modifications de configuration. Pour en savoir plus, consultez Gérer vous-même les modifications de configuration.

L'utilisateur s'attend également à ce que l'état de votre activité reste le même s'il passe temporairement à une autre application, puis revient plus tard à la vôtre. Par exemple, l'utilisateur effectue une recherche dans votre activité de recherche, puis appuie sur le bouton d'accueil ou répond à un appel téléphonique. Lorsqu'il revient à l'activité de recherche, il s'attend à y trouver le mot clé et les résultats de la recherche tels qu'il les a laissés.

Dans ce cas, votre application est déplacée en arrière-plan. Le système s'efforce de garder l'activité de votre application en mémoire. Cependant, le système peut détruire cette activité si l'utilisateur quitte votre application pour interagir avec d'autres. Dans ce cas, l'instance d'activité est détruite, ainsi que tout état qui y est stocké. Lorsque l'utilisateur redémarre l'application, l'activité s'est réinitialisée, contrairement à ses attentes. Pour en savoir plus sur l'arrêt du processus, consultez le cycle de vie des processus et de l'application.

Options pour conserver l'état de l'UI

Lorsque les attentes de l'utilisateur concernant l'état de l'interface utilisateur ne correspondent pas au comportement par défaut du système, vous devez enregistrer et restaurer l'état de l'interface utilisateur pour vous assurer que la destruction déclenchée par le système est transparente pour l'utilisateur.

Chacune des options permettant de conserver l'état de l'interface utilisateur dépend des dimensions suivantes, avec des conséquences sur l'expérience utilisateur :

ViewModel SavedInstanceState Stockage persistant
Emplacement de stockage En mémoire En mémoire Sur le disque ou le réseau
Conserver après modification de configuration Oui Oui Oui
Conserver après arrêt du processus initié par le système Non Oui Oui
Conserver après arrêt complet de l'activité/onFinish() Non Non Oui
Limitations liées aux données Les objets complexes sont acceptés, mais l'espace est limité par la mémoire disponible Types primitifs et petits objets simples tels que String uniquement Limité uniquement par l'espace disque ou le coût/temps de récupération de la ressource réseau
Temps de lecture/écriture Rapide (accès à la mémoire uniquement) Lent (sérialisation/désérialisation requise) Lent (transaction réseau ou accès au disque requis)

Gérer les modifications de configuration avec ViewModel

ViewModel est la solution idéale pour stocker et gérer les données liées à l'interface utilisateur lorsque l'utilisateur est actif sur l'application. Elle permet d'accéder rapidement aux données de l'interface utilisateur et vous évite d'extraire à nouveau les données du réseau ou du disque en cas de rotation, de redimensionnement de la fenêtre et d'autres modifications de configuration courantes. Pour apprendre à implémenter un ViewModel, consultez le guide ViewModel.

ViewModel conserve les données en mémoire, qui sont donc moins coûteuses à extraire que les données du disque ou du réseau. Un ViewModel est associé à une activité (ou à un autre propriétaire de cycle de vie). Il reste en mémoire en cas de modification de configuration et le système associe automatiquement le ViewModel à la nouvelle instance d'activité résultant de la modification de configuration.

Les éléments ViewModel sont automatiquement détruits par le système lorsque l'utilisateur quitte votre activité ou fragment, ou si vous appelez finish(). Dans ces cas de figure, l'état est donc effacé comme s'y attend l'utilisateur.

Contrairement à l'état d'instance enregistré, les éléments ViewModel sont détruits en cas d'arrêt d'un processus déclenché par le système. Pour actualiser les données après un arrêt de processus déclenché par le système dans ViewModel, utilisez l'API SavedStateHandle. Si les données sont liées à l'UI et n'ont pas besoin d'être conservées dans ViewModel, utilisez onSaveInstanceState() dans le système View ou rememberSaveable dans Jetpack Compose. S'il s'agit de données d'application, il est préférable de les conserver sur le disque.

Si vous disposez déjà d'une solution en mémoire pour stocker l'état de l'UI en cas de modifications de la configuration, vous n'aurez peut-être pas besoin d'utiliser ViewModel.

Utiliser l'état d'instance enregistré comme sauvegarde pour gérer un arrêt de processus déclenché par le système

Le rappel onSaveInstanceState() dans le système View, rememberSaveable dans Jetpack Compose et SavedStateHandle dans ViewModel stockent les données nécessaires pour actualiser l'état d'un contrôleur d'UI, tel qu'une activité ou un fragment, si le système détruit, puis recrée ce contrôleur. Pour découvrir comment intégrer l'état d'instance enregistré à l'aide de onSaveInstanceState, consultez Enregistrer et restaurer l'état d'une activité dans le Guide du cycle de vie d'une activité.

Les bundles d'états d'instances enregistrés sont conservés en cas de modification de la configuration et d'arrêt du processus, mais sont limités par le stockage et la vitesse, car les différentes API sérialisent les données. La sérialisation peut utiliser beaucoup de mémoire si les objets sérialisés sont complexes. Étant donné que ce processus se produit sur le thread principal lors d'une modification de la configuration, une sérialisation de longue durée peut entraîner la perte de frames et un stuttering visuel.

N'utilisez pas l'état d'instance enregistré pour stocker de grandes quantités de données, telles que des bitmaps, ni des structures de données complexes nécessitant une longue sérialisation ou désérialisation. Stockez plutôt les types primitifs et les petits objets simples comme String. Utilisez donc l'état d'instance enregistré pour stocker une quantité minimale de données nécessaire, par exemple un identifiant, afin de recréer les données nécessaires pour restaurer l'état précédent de l'UI si les autres mécanismes de persistance échouent. La plupart des applications doivent implémenter cela pour gérer un arrêt de processus déclenché par le système.

En fonction des cas d'utilisation de votre application, vous n'aurez peut-être pas besoin d'utiliser l'état d'instance enregistré. Par exemple, un navigateur peut renvoyer l'utilisateur à la page exacte qu'il consultait avant sa fermeture. Si votre activité se comporte de cette manière, vous pouvez renoncer à utiliser l'état d'instance enregistré et tout conserver en local.

En outre, lorsque vous lancez une activité à partir d'un intent, le bundle d'extras est envoyé à l'activité à la fois lorsque la configuration change et lorsque le système restaure l'activité. Si des données d'état de l'UI, telles qu'une requête de recherche, ont été transmises en tant qu'intent supplémentaire lors du lancement de l'activité, vous pouvez utiliser le bundle d'extras au lieu du bundle d'états d'instances enregistrés. Pour en savoir plus sur les extras d'intent, consultez Intents et filtres d'intents.

Dans les deux cas, vous devez toujours utiliser un ViewModel pour éviter de gaspiller des cycles d'actualisation des données à partir de la base de données en cas de modification de la configuration.

Si les données d'UI à conserver sont simples et légères, vous pouvez utiliser les API d'état d'instance enregistré de manière isolée pour conserver vos données d'état.

Associer à l'état enregistré à l'aide de SavedStateRegistry

À partir de Fragment 1.1.0 ou de sa dépendance transitive Activity 1.0.0, les contrôleurs d'interface utilisateur, tels que Activity ou Fragment, intègrent SavedStateRegistryOwner et fournissent un élément SavedStateRegistry associé à ce contrôleur. SavedStateRegistry permet aux composants de s'associer à l'état enregistré de votre contrôleur d'interface utilisateur pour être utilisé ou y contribuer. Par exemple, le module d'état enregistré pour ViewModel utilise SavedStateRegistry pour créer un élément SavedStateHandle et le fournir à vos objets ViewModel. Vous pouvez extraire SavedStateRegistry à partir de votre contrôleur d'interface utilisateur en appelant getSavedStateRegistry().

Les composants qui contribuent à l'état enregistré doivent intégrer SavedStateRegistry.SavedStateProvider, qui définit une seule méthode appelée saveState(). La méthode saveState() permet à votre composant de renvoyer un élément Bundle contenant tout état à enregistrer à partir de ce composant. SavedStateRegistry appelle cette méthode pendant la phase d'enregistrement du cycle de vie du contrôleur d'interface utilisateur.

Kotlin

class SearchManager : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val QUERY = "query"
    }

    private val query: String? = null

    ...

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String QUERY = "query";
    private String query = null;
    ...

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }
}

Pour enregistrer un élément SavedStateProvider, appelez registerSavedStateProvider() au niveau de l'élément SavedStateRegistry en transmettant une clé à associer aux données du fournisseur et au fournisseur lui-même. Les données précédemment enregistrées pour le fournisseur peuvent être extraites à partir de l'état enregistré en appelant consumeRestoredStateForKey() sur l'élément SavedStateRegistry et en transmettant la clé associée aux données du fournisseur.

Dans un élément Activity ou Fragment, vous pouvez enregistrer un élément SavedStateProvider dans onCreate() après avoir appelé super.onCreate(). Vous pouvez également définir un élément LifecycleObserver au niveau d'un élément SavedStateRegistryOwner, qui intègre LifecycleOwner, puis enregistrer SavedStateProvider une fois que l'événement ON_CREATE se produit. En utilisant un élément LifecycleObserver, vous pouvez dissocier l'enregistrement et l'extraction de l'état enregistré auparavant à partir de l'élément SavedStateRegistryOwner lui-même.

Kotlin

class SearchManager(registryOwner: SavedStateRegistryOwner) : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val PROVIDER = "search_manager"
        private const val QUERY = "query"
    }

    private val query: String? = null

    init {
        // Register a LifecycleObserver for when the Lifecycle hits ON_CREATE
        registryOwner.lifecycle.addObserver(LifecycleEventObserver { _, event ->
            if (event == Lifecycle.Event.ON_CREATE) {
                val registry = registryOwner.savedStateRegistry

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this)

                // Get the previously saved state and restore it
                val state = registry.consumeRestoredStateForKey(PROVIDER)

                // Apply the previously saved state
                query = state?.getString(QUERY)
            }
        }
    }

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }

    ...
}

class SearchFragment : Fragment() {
    private var searchManager = SearchManager(this)
    ...
}

Java

class SearchManager implements SavedStateRegistry.SavedStateProvider {
    private static String PROVIDER = "search_manager";
    private static String QUERY = "query";
    private String query = null;

    public SearchManager(SavedStateRegistryOwner registryOwner) {
        registryOwner.getLifecycle().addObserver((LifecycleEventObserver) (source, event) -> {
            if (event == Lifecycle.Event.ON_CREATE) {
                SavedStateRegistry registry = registryOwner.getSavedStateRegistry();

                // Register this object for future calls to saveState()
                registry.registerSavedStateProvider(PROVIDER, this);

                // Get the previously saved state and restore it
                Bundle state = registry.consumeRestoredStateForKey(PROVIDER);

                // Apply the previously saved state
                if (state != null) {
                    query = state.getString(QUERY);
                }
            }
        });
    }

    @NonNull
    @Override
    public Bundle saveState() {
        Bundle bundle = new Bundle();
        bundle.putString(QUERY, query);
        return bundle;
    }

    ...
}

class SearchFragment extends Fragment {
    private SearchManager searchManager = new SearchManager(this);
    ...
}

Gérer l'arrêt du processus pour des données complexes ou volumineuses à l'aide de la persistance locale

Le stockage local persistant, tel qu'une base de données ou des préférences partagées, sera conservé tant que votre application est installée sur l'appareil de l'utilisateur (sauf si celui-ci efface les données de votre application). Bien que ce stockage local soit conservé après une activité lancée par le système et l'arrêt du processus d'application, il peut être coûteux de le récupérer, car il devra être lu en mémoire depuis le stockage local. Souvent, ce stockage local persistant fait déjà partie de l'architecture de votre application pour enregistrer toutes les données que vous ne voulez pas perdre si vous lancez et fermez l'activité.

Ni l'élément ViewModel ni l'état de l'instance enregistré ne sont des solutions de stockage à long terme. Ils ne remplacent donc pas le stockage local, tel qu'une base de données. Utilisez plutôt ces mécanismes pour stocker temporairement l'état transitoire de l'interface utilisateur et le stockage persistant pour les autres données d'application. Consultez le guide sur l'architecture des applications pour découvrir comment exploiter le stockage local afin de conserver les données du modèle de votre application à long terme (par exemple, en cas de redémarrages de l'appareil).

Gérer l'état de l'UI : diviser pour mieux régner

Vous pouvez enregistrer et restaurer l'état de l'interface utilisateur de manière efficace en répartissant le travail entre les différents types de mécanismes de persistance. Dans la plupart des cas, chacun de ces mécanismes doit stocker un type de données différent utilisé dans l'activité en fonction des avantages en termes de complexité des données, de vitesse d'accès et de durée de vie :

  • Persistance locale : stocke toutes les données d'application que vous ne voulez pas perdre si vous ouvrez et fermez l'activité.
    • Par exemple, un ensemble d'objets "chansons" pouvant inclure des fichiers audio et des métadonnées.
  • ViewModel : stocke en mémoire toutes les données nécessaires à l'affichage de l'UI associée, à savoir l'état d'UI de l'écran.
    • Par exemple, les objets "chansons" de la recherche et de la requête de recherche les plus récents.
  • État d'instance enregistré : stocke une petite quantité de données nécessaire pour actualiser l'état de l'UI en cas d'arrêt du système, puis recrée l'UI. Au lieu de stocker ici des objets complexes, conservez-les dans un espace de stockage local et stockez un identifiant unique pour ces objets dans les API d'état d'instance enregistré.
    • Par exemple, stocker la requête de recherche la plus récente.

Prenons l'exemple d'une activité qui vous permet d'effectuer une recherche dans votre bibliothèque de chansons. Voici comment gérer différents événements :

Lorsque l'utilisateur ajoute une chanson, ViewModel délègue immédiatement la persistance de ces données localement. Si la chanson nouvellement ajoutée doit être affichée dans l'interface utilisateur, vous devez également mettre à jour les données de l'objet ViewModel pour prendre en compte l'ajout du titre. N'oubliez pas d'insérer toutes les bases de données en dehors du thread principal.

Lorsque l'utilisateur recherche une chanson, les données de chanson que vous chargez depuis la base de données, aussi complexes soient-elles, doivent être immédiatement stockées dans l'objet ViewModel dans le cadre de l'état d'UI de l'écran.

Lorsque l'activité passe en arrière-plan et que le système appelle les API d'état d'instance enregistré, la requête de recherche doit être stockée dans l'état d'instance enregistré, au cas où le processus serait recréé. Étant donné que les informations sont nécessaires pour charger les données d'application qui y sont conservées, stockez la requête de recherche dans le SavedStateHandle du ViewModel. Il s'agit de toutes les informations dont vous avez besoin pour charger les données et rétablir l'état actuel de l'UI.

Restaurer les états complexes : recomposer le puzzle

Lorsque l'utilisateur doit revenir à l'activité, il existe deux cas de figure possibles pour la recréer :

  • L'activité est recréée après avoir été arrêtée par le système. La requête est enregistrée dans un bundle d'états d'instances enregistrés. L'UI doit transmettre la requête à ViewModel si SavedStateHandle n'est pas utilisé. ViewModel constate qu'aucun résultat de recherche n'est mis en cache et qu'aucun délégué ne charge les résultats de recherche à l'aide de la requête de recherche donnée.
  • L'activité est créée après une modification de la configuration. Comme l'instance ViewModel n'a pas été détruite, ViewModel contient toutes les informations mises en cache dans la mémoire et n'a pas besoin d'interroger à nouveau la base de données.

Ressources supplémentaires

Pour en savoir plus sur l'enregistrement des états de l'interface utilisateur, consultez les ressources ci-dessous.

Blogs