Salva stato UI in Compose

A seconda di dove viene eseguito l'hoisting dello stato e della logica richiesta, puoi utilizzare API diverse per archiviare e ripristinare lo stato della UI. Ogni app utilizza una combinazione di API per raggiungere questo obiettivo nel modo migliore.

Qualsiasi app per Android potrebbe perdere lo stato della UI a causa della ricreazione di attività o processi. Questa perdita di stato può verificarsi a causa dei seguenti eventi:

La conservazione dello stato dopo questi eventi è essenziale per un'esperienza utente positiva. La scelta dello stato da mantenere dipende dai flussi utente unici della tua app. Come best practice, devi almeno conservare l'input dell'utente e lo stato relativo alla navigazione. Ad esempio, la posizione di scorrimento di un elenco, l'ID dell'elemento di cui l'utente vuole maggiori dettagli, la selezione in corso delle preferenze utente o l'input nei campi di testo.

Questa pagina riassume le API disponibili per archiviare lo stato della UI a seconda di dove viene eseguito l'hoisting dello stato e della logica che lo richiede.

Logica UI

Se lo stato viene sottoposto ad hoisting nella UI, nelle funzioni componibili o nelle classi di contenitori di stato semplici con ambito nella composizione, puoi utilizzare rememberSaveable per conservare lo stato durante la ricreazione di attività e processi.

Nello snippet seguente, rememberSaveable viene utilizzato per archiviare un singolo stato booleano di un elemento UI:

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

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

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

Figura 1. Il fumetto del messaggio chat si espande e si comprime quando viene toccato.

showDetails è una variabile booleana che memorizza se il fumetto della chat è compresso o espanso.

rememberSaveable memorizza lo stato dell'elemento UI in un Bundle tramite il meccanismo dello stato dell'istanza salvato.

È in grado di archiviare automaticamente i tipi primitivi nel bundle. Se lo stato è contenuto in un tipo non primitivo, ad esempio una classe di dati, puoi utilizzare diversi meccanismi di archiviazione, ad esempio l'annotazione Parcelize, le API Compose come listSaver e mapSaver o l'implementazione di una classe di salvataggio personalizzata che estende la classe Saver di runtime di Compose. Per scoprire di più su questi metodi, consulta la documentazione sui modi per archiviare lo stato.

Nel seguente snippet, l'API Compose rememberLazyListState archivia LazyListState, che consiste nello stato di scorrimento di un LazyColumn o LazyRow, utilizzando rememberSaveable. Utilizza un LazyListState.Saver, ovvero un saver personalizzato in grado di archiviare e ripristinare lo stato di scorrimento. Dopo la ricreazione di un'attività o di un processo (ad esempio, dopo una modifica della configurazione come il cambio di orientamento del dispositivo), lo stato di scorrimento viene conservato.

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

Best practice

rememberSaveable utilizza un Bundle per archiviare lo stato della UI, che viene condiviso da altre API che scrivono anche al suo interno, come le chiamate onSaveInstanceState() nella tua attività. Tuttavia, le dimensioni di questo Bundle sono limitate e l'archiviazione di oggetti di grandi dimensioni potrebbe causare eccezioni TransactionTooLarge in fase di runtime. Questo può essere particolarmente problematico nelle app Activity a pagina singola in cui viene utilizzato lo stesso Bundle in tutta l'app.

Per evitare questo tipo di arresto anomalo, non devi archiviare oggetti complessi di grandi dimensioni o elenchi di oggetti nel bundle.

Memorizza invece lo stato minimo richiesto, come ID o chiavi, e utilizzali per delegare il ripristino di uno stato della UI più complesso ad altri meccanismi, come l'archiviazione permanente.

Queste scelte di progettazione dipendono dai casi d'uso specifici della tua app e dal comportamento che gli utenti si aspettano.

Verifica il ripristino dello stato

Puoi verificare che lo stato memorizzato con rememberSaveable negli elementi Compose venga ripristinato correttamente quando l'attività o il processo viene ricreato. Esistono API specifiche per raggiungere questo obiettivo, ad esempio StateRestorationTester. Per saperne di più, consulta la documentazione relativa ai test.

Logica di business

Se lo stato dell'elemento UI viene sottoposto ad hoisting in ViewModel perché è richiesto dalla logica di business, puoi utilizzare le API di ViewModel.

Uno dei principali vantaggi dell'utilizzo di un ViewModel nella tua app per Android è che gestisce le modifiche alla configurazione senza costi aggiuntivi. Quando si verifica una modifica della configurazione e l'attività viene eliminata e ricreata, lo stato della UI di cui è stato eseguito l'innalzamento in ViewModel viene conservato in memoria. Dopo la ricreazione, l'istanza ViewModel precedente viene collegata alla nuova istanza dell'attività.

Tuttavia, un'istanza ViewModel non sopravvive all'interruzione del processo avviata dal sistema. Per fare in modo che lo stato della UI sopravviva a questo evento, utilizza il modulo Saved State per ViewModel, che contiene l'API SavedStateHandle.

Best practice

SavedStateHandle utilizza anche il meccanismo Bundle per archiviare lo stato della UI, quindi dovresti utilizzarlo solo per archiviare lo stato degli elementi UI semplici.

Lo stato della UI della schermata, che viene prodotto applicando regole aziendali e accedendo a livelli dell'applicazione diversi dalla UI, non deve essere archiviato in SavedStateHandle a causa della sua potenziale complessità e dimensione. Puoi utilizzare meccanismi diversi per archiviare dati complessi o di grandi dimensioni, come l'archiviazione permanente locale. Dopo la ricreazione di un processo, la schermata viene ricreata con lo stato temporaneo ripristinato che era stato memorizzato in SavedStateHandle (se presente) e lo stato della UI della schermata viene generato di nuovo dal livello dati.

API SavedStateHandle

SavedStateHandle dispone di diverse API per archiviare lo stato degli elementi UI, in particolare:

Scrivi State saveable()
StateFlow getStateFlow()

Scrivi State

Utilizza l'API saveable di SavedStateHandle per leggere e scrivere lo stato dell'elemento UI come MutableState, in modo che sopravviva alla ricreazione di attività e processi con una configurazione minima del codice.

L'API saveable supporta i tipi primitivi in modo predefinito e riceve un parametro stateSaver per utilizzare i saver personalizzati, proprio come rememberSaveable().

Nel seguente snippet, message memorizza l'input utente digitato in 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) }
    )
}

Per saperne di più sull'utilizzo dell'API saveable, consulta la documentazione di SavedStateHandle.

StateFlow

Utilizza getStateFlow() per archiviare lo stato dell'elemento UI e utilizzarlo come flusso da SavedStateHandle. StateFlow è di sola lettura e l'API richiede di specificare una chiave per poter sostituire il flusso ed emettere un nuovo valore. Con la chiave che hai configurato, puoi recuperare il StateFlow e raccogliere il valore più recente.

Nel seguente snippet, savedFilterType è una variabile StateFlow che memorizza un tipo di filtro applicato a un elenco di canali di chat in un'app di 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
}

Ogni volta che l'utente seleziona un nuovo tipo di filtro, viene chiamato setFiltering. In questo modo viene salvato un nuovo valore in SavedStateHandle memorizzato con la chiave _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType è un flusso che emette l'ultimo valore memorizzato nella chiave. filteredChannels è iscritto al flusso per eseguire il filtro dei canali.

Per saperne di più sull'API getStateFlow(), consulta la documentazione di SavedStateHandle.

Riepilogo

La tabella seguente riassume le API trattate in questa sezione e quando utilizzarle per salvare lo stato della UI:

Evento Logica UI Logica di business in un ViewModel
Modifiche alla configurazione rememberSaveable Automatico
Interruzione del processo avviata dal sistema rememberSaveable SavedStateHandle

L'API da utilizzare dipende da dove viene conservato lo stato e dalla logica che richiede. Per lo stato utilizzato nella logica della UI, utilizza rememberSaveable. Per lo stato utilizzato nella logica di business, se lo conservi in un ViewModel, salvalo utilizzando SavedStateHandle.

Devi utilizzare le API bundle (rememberSaveable e SavedStateHandle) per archiviare piccole quantità di stato della UI. Questi dati sono il minimo necessario per ripristinare lo stato precedente della UI, insieme ad altri meccanismi di archiviazione. Ad esempio, se memorizzi nel bundle l'ID di un profilo che l'utente stava visualizzando, puoi recuperare dati pesanti, come i dettagli del profilo, dal livello dati.

Per saperne di più sui diversi modi per salvare lo stato della UI, consulta la documentazione generale sul salvataggio dello stato della UI e la pagina relativa al livello dati della guida all'architettura.