Enregistrer l'état de l'UI dans Compose

Selon l'emplacement sur lequel l'état est hissé et selon la logique requise, différentes API permettent de stocker et de restaurer l'état de l'UI. Pour ce faire, chaque application utilise une combinaison d'API.

Toute application Android est susceptible de perdre son état d'UI en raison de la recréation de l'activité ou du processus. Cette perte d'état peut être causée par les événements suivants :

La préservation de l'état après ces événements est essentielle pour une expérience utilisateur positive. Le choix de l'état qui perdurera dépend des parcours utilisateur uniques de votre application. Nous vous recommandons de conserver au moins l'entrée utilisateur et l'état lié à la navigation. Il peut s'agir de la position de défilement d'une liste, de l'ID de l'élément pour lequel l'utilisateur souhaite obtenir plus d'informations, de la sélection en cours des préférences utilisateur ou de la saisie de données dans des champs de texte.

Cette page récapitule les API disponibles pour stocker l'état de l'UI en fonction de l'emplacement sur lequel l'état est hissé et de la logique sous-jacente.

Logique d'UI

Si l'état est hissé dans l'UI, que ce soit dans des fonctions modulables ou des classes de conteneurs d'état simples limitées à la composition, vous pouvez utiliser rememberSaveable pour conserver l'état de la recréation de l'activité et du processus.

Dans l'extrait de code suivant, rememberSaveable permet de stocker un seul état booléen pour l'élément d'interface utilisateur :

@Composable
fun ChatBubble(
    message: Message
) {
    var showDetails by rememberSaveable { mutableStateOf(false) }

    ClickableText(
        text = AnnotatedString(message.content),
        onClick = { showDetails = !showDetails }
    )

    if (showDetails) {
        Text(message.timestamp)
    }
}

Figure 1 : L'info-bulle de message se développe et se réduit lorsque l'utilisateur appuie dessus

showDetails est une variable booléenne qui est stockée si l'info-bulle de chat est réduite ou développée.

rememberSaveable stocke l'état de l'élément d'interface utilisateur dans un Bundle via le mécanisme d'enregistrement de l'état d'instance.

Il peut stocker automatiquement des types primitifs dans le groupe. Si l'état est conservé dans un type qui n'est pas primitif, comme une classe de données, vous pouvez utiliser différents mécanismes de stockage, comme l'annotation Parcelize, avec des API Compose telles que listSaver et mapSaver, ou via l'implémentation d'une classe de saver personnalisé qui étend la classe Saver de l'environnement d'exécution Compose. Pour en savoir plus sur ces méthodes, consultez la documentation intitulée Comment stocker l'état.

Dans l'extrait de code suivant, l'API Compose rememberLazyListState stocke LazyListState, qui est l'état de défilement d'un objet LazyColumn ou LazyRow, avec rememberSaveable. Elle utilise un LazyListState.Saver, qui est un saver personnalisé capable de stocker et de restaurer l'état de défilement. Après la recréation d'une activité ou d'un processus (par exemple, après un changement de configuration telle que la modification de l'orientation de l'appareil), l'état de défilement est préservé.

@Composable
fun rememberLazyListState(
    initialFirstVisibleItemIndex: Int = 0,
    initialFirstVisibleItemScrollOffset: Int = 0
): LazyListState {
    return rememberSaveable(saver = LazyListState.Saver) {
        LazyListState(
            initialFirstVisibleItemIndex, initialFirstVisibleItemScrollOffset
        )
    }
}

Bonne pratique

rememberSaveable utilise un Bundle pour stocker l'état de l'UI. Celui-ci est partagé par d'autres API qui en tirent également parti, comme les appels onSaveInstanceState() dans l'activité. Cependant, la taille de ce Bundle est limitée, et le stockage d'objets volumineux peut entraîner des exceptions TransactionTooLarge lors de l'exécution. Cela peut s'avérer particulièrement problématique dans les applications Activity uniques dans lesquelles un seul et même Bundle est utilisé.

Pour éviter ce type de plantage, évitez de stocker des objets ou des listes d'objets complexes et volumineux dans le bundle.

Stockez plutôt l'état minimal requis, tel que les ID ou les clés, et utilisez-le pour déléguer la restauration d'un état d'UI plus complexe à d'autres mécanismes, tels que le stockage persistant.

Ces choix de conception dépendent des cas d'utilisation spécifiques de votre application et du comportement attendu par vos utilisateurs.

Vérifier la restauration d'état

Vous pouvez vérifier que l'état stocké avec rememberSaveable dans vos éléments Compose est correctement restauré lorsque l'activité ou le processus est recréé. Pour cela, il existe des API spécifiques, telles que StateRestorationTester. Pour en savoir plus, consultez la documentation spécifique aux tests.

Logique métier

Si l'état de votre élément d'interface utilisateur est hissé dans ViewModel, car il est requis par la logique métier, vous pouvez utiliser les API de ViewModel.

L'un des principaux avantages de l'utilisation d'un ViewModel dans une application Android est qu'il permet de gérer sans frais les modifications de configuration. En cas de modification de la configuration, et si l'activité est détruite et recréée, l'état de l'UI hissé dans ViewModel est conservé en mémoire. Après la recréation, l'ancienne instance de ViewModel est associée à la nouvelle instance d'activité.

Toutefois, une instance ViewModel ne survit pas à l'arrêt d'un processus initié par le système. Pour que l'état de l'UI persiste, utilisez le module Saved State pour ViewModel, qui contient l'API SavedStateHandle.

Bonne pratique

SavedStateHandle utilise également le mécanisme Bundle pour stocker l'état de l'UI. Vous ne devez donc y recourir que pour stocker un état d'élément d'interface utilisateur simple.

L'état de l'UI de l'écran, qui est obtenu en appliquant des règles métier et en accédant à des couches de votre application autre que l'UI, ne doit pas être stocké dans SavedStateHandle en raison des problèmes potentiels en termes de complexité et de taille. Vous pouvez utiliser différents mécanismes pour stocker des données complexes ou volumineuses, comme le stockage persistant local. Après la recréation d'un processus, l'état temporaire de l'écran qui était stocké dans SavedStateHandle (le cas échéant) est restauré, et l'état de l'UI de l'écran est à nouveau généré à partir de la couche de données.

API SavedStateHandle

SavedStateHandle dispose de différentes API permettant de stocker l'état des éléments de l'interface utilisateur, en particulier :

Compose State saveable()
StateFlow getStateFlow()

Compose State

Utilisez l'API saveable de SavedStateHandle pour lire et écrire l'état de l'élément d'interface utilisateur en tant que MutableState, pour qu'il persiste après la recréation de l'activité et du processus avec une configuration minimale du code.

L'API saveable prend directement en charge les types primitifs et reçoit un paramètre stateSaver pour utiliser les savers personnalisés, tout comme rememberSaveable().

Dans l'extrait de code suivant, message stocke les types d'entrée utilisateur dans un TextField :

class ConversationViewModel(
    savedStateHandle: SavedStateHandle
) : ViewModel() {

    var message by savedStateHandle.saveable(stateSaver = TextFieldValue.Saver) {
        mutableStateOf(TextFieldValue(""))
    }
        private set

    fun update(newMessage: TextFieldValue) {
        message = newMessage
    }

    /*...*/
}

val viewModel = ConversationViewModel(SavedStateHandle())

@Composable
fun UserInput(/*...*/) {
    TextField(
        value = viewModel.message,
        onValueChange = { viewModel.update(it) }
    )
}

Pour en savoir plus sur l'utilisation de l'API saveable, consultez la documentation SavedStateHandle.

StateFlow

Optez pour getStateFlow() afin de stocker l'état des éléments d'interface utilisateur et de l'utiliser en tant que flux à partir de SavedStateHandle. StateFlow est en lecture seule, et l'API exige que vous spécifiiez une clé afin que vous puissiez remplacer le flux pour émettre une nouvelle valeur. Avec la clé que vous avez configurée, vous pouvez récupérer StateFlow et collecter la dernière valeur.

Dans l'extrait de code suivant, savedFilterType est une variable StateFlow qui stocke un type de filtre appliqué à une liste de canaux d'une application de chat :

private const val CHANNEL_FILTER_SAVED_STATE_KEY = "ChannelFilterKey"

class ChannelViewModel(
    channelsRepository: ChannelsRepository,
    private val savedStateHandle: SavedStateHandle
) : ViewModel() {

    private val savedFilterType: StateFlow<ChannelsFilterType> = savedStateHandle.getStateFlow(
        key = CHANNEL_FILTER_SAVED_STATE_KEY, initialValue = ChannelsFilterType.ALL_CHANNELS
    )

    private val filteredChannels: Flow<List<Channel>> =
        combine(channelsRepository.getAll(), savedFilterType) { channels, type ->
            filter(channels, type)
        }.onStart { emit(emptyList()) }

    fun setFiltering(requestType: ChannelsFilterType) {
        savedStateHandle[CHANNEL_FILTER_SAVED_STATE_KEY] = requestType
    }

    /*...*/
}

enum class ChannelsFilterType {
    ALL_CHANNELS, RECENT_CHANNELS, ARCHIVED_CHANNELS
}

Chaque fois que l'utilisateur sélectionne un nouveau type de filtre, setFiltering est appelé. Cela permet d'enregistrer une nouvelle valeur dans SavedStateHandle, stockée avec la clé _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType est un flux émettant la dernière valeur stockée dans la clé. filteredChannels est abonné au flux pour effectuer le filtrage des canaux.

Pour en savoir plus sur l'API getStateFlow(), consultez la documentation SavedStateHandle.

Résumé

Le tableau suivant récapitule les API abordées dans cette section et indique quand les utiliser pour enregistrer l'état de l'UI :

Événement Logique d'UI Logique métier dans un ViewModel
Modifications de la configuration rememberSaveable Automatique
Arrêt de processus initié par le système rememberSaveable SavedStateHandle

L'API à utiliser dépend de l'emplacement de l'état et de la logique requise. Pour l'état utilisé dans la logique de l'interface utilisateur, optez pour rememberSaveable. Pour l'état utilisé dans la logique métier, enregistrez-le à l'aide de SavedStateHandle s'il se trouve dans un ViewModel.

Utilisez les API de bundle (rememberSaveable et SavedStateHandle) pour stocker un faible volume de données correspondant à l'état de l'UI. Ces données constituent le minimum nécessaire pour restaurer l'état d'UI précédent, en plus d'autres mécanismes de stockage. Par exemple, si vous stockez dans le bundle l'ID d'un profil consulté par l'utilisateur, vous pouvez récupérer des données volumineuses (telles que des détails du profil) à partir de la couche de données.

Pour en savoir plus sur les différentes façons d'enregistrer l'état de l'UI, consultez la documentation sur l'enregistrement de l'état de l'UI et la page sur la couche de données du guide de l'architecture.