Salva stato UI in Compose

A seconda di dove viene sollevato lo stato e della logica richiesta, puoi utilizzare API diverse per archiviare e ripristinare lo stato dell'UI. Ogni app utilizza una combinazione di API per ottenere il risultato migliore.

Qualsiasi app per Android potrebbe perdere il suo stato dell'UI a causa della ricreazione dell'attività o del processo. Questa perdita di stato può verificarsi a causa dei seguenti eventi:

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

Questa pagina riassume le API disponibili per archiviare lo stato dell'UI a seconda di dove viene sollevato lo stato e della logica che lo richiede.

Logica dell'UI

Se lo stato viene sollevato nell'UI, nelle funzioni componibili o nelle classi di contenitori di stato semplici con ambito di composizione, puoi utilizzare rememberSaveable per mantenere lo stato durante la ricreazione dell'attività e del processo.

Nel seguente snippet, rememberSaveable viene utilizzato per archiviare lo stato di un singolo elemento dell'UI booleano:

@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. La bolla del messaggio di chat si espande e si comprime quando viene toccata.

showDetails è una variabile booleana che memorizza se la bolla di chat è compressa o espansa.

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

È 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 meccanismi di archiviazione diversi, 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 saperne di più su questi metodi, consulta la documentazione Modalità di archiviazione dello stato.

Nel seguente snippet, l'rememberLazyListState Compose API archivia LazyListState, che consiste nello stato di scorrimento di un LazyColumn o LazyRow, utilizzando rememberSaveable. Utilizza un LazyListState.Saver, ovvero un salvataggio 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 alla configurazione come la modifica dell'orientamento del dispositivo), lo stato di scorrimento viene mantenuto.

@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 dell'UI, che viene condiviso da altre API che scrivono anche in esso, come le chiamate onSaveInstanceState() nell' attività. Tuttavia, la dimensione di questo Bundle è limitata e l'archiviazione di oggetti di grandi dimensioni potrebbe generare eccezioni TransactionTooLarge in fase di runtime. Questo può essere particolarmente problematico nelle app con una singola Activity in cui lo stesso Bundle viene utilizzato 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.

Archivia invece lo stato minimo richiesto, come ID o chiavi, e utilizzali per delegare il ripristino di uno stato dell'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 previsto dagli utenti.

Verificare il ripristino dello stato

Puoi verificare che lo stato archiviato 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 sui test.

Logica di business

Se lo stato dell'elemento dell'UI viene sollevato nel 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 senza costi le modifiche alla configurazione. Quando si verifica una modifica alla configurazione e l'attività viene eliminata e ricreata, lo stato dell'UI sollevato nel ViewModel viene mantenuto in memoria. Dopo la ricreazione, la vecchia istanza di ViewModel viene collegata alla nuova istanza di attività.

Tuttavia, un'istanza di ViewModel non sopravvive all'interruzione del processo avviata dal sistema. Per fare in modo che lo stato dell'UI sopravviva, utilizza il modulo Stato salvato per ViewModel, che contiene l'API SavedStateHandle.

Best practice

SavedStateHandle utilizza anche il meccanismo Bundle per archiviare lo stato dell'UI, quindi dovresti utilizzarlo solo per archiviare lo stato semplice dell'elemento dell'UI .

Lo stato dell'UI della schermata, che viene prodotto applicando regole di business e accedendo a livelli dell'applicazione diversi dall'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, ad esempio l'archiviazione permanente locale storage. Dopo la ricreazione di un processo, la schermata viene ricreata con lo stato temporaneo ripristinato archiviato in SavedStateHandle (se presente) e lo stato dell'UI della schermata viene prodotto di nuovo dal livello dati.

API SavedStateHandle

SavedStateHandle ha diverse API per archiviare lo stato dell'elemento dell'UI, in particolare:

Compose State saveable()
StateFlow getStateFlow()

State di Compose

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

L'API saveable supporta i tipi primitivi out-of-the-box e riceve un parametro stateSaver per utilizzare salvataggi personalizzati, proprio come rememberSaveable().

Nel seguente snippet, message memorizza l'input dell'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 ulteriori informazioni sull'utilizzo dell'API saveable, consulta la documentazione di SavedStateHandle.

StateFlow

Utilizza getStateFlow() per archiviare lo stato dell'elemento dell'UI e utilizzarlo come flusso da SavedStateHandle. The StateFlow è di sola lettura, e l'API richiede di specificare una chiave in modo da poter sostituire il flusso per emettere un nuovo valore. Con la chiave configurata, puoi recuperare StateFlow e raccogliere l'ultimo valore.

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 chiamata setFiltering. In questo modo viene salvato un nuovo valore in SavedStateHandle archiviato con la chiave _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType è un flusso che emette l'ultimo valore archiviato nella chiave. filteredChannels è sottoscritto al flusso per eseguire il filtraggio dei canali.

Per ulteriori informazioni sull'API getStateFlow(), consulta la documentazione di SavedStateHandle.

Riepilogo

La seguente tabella riassume le API trattate in questa sezione e quando utilizzare ciascuna per salvare lo stato dell'UI:

Evento Logica dell'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 mantenuto lo stato e dalla logica che richiede. Per lo stato utilizzato nella logica dell'UI, utilizza rememberSaveable. Per lo stato utilizzato nella logica di business, se lo mantieni in un ViewModel, salvalo utilizzando SavedStateHandle.

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

Per ulteriori informazioni sui diversi modi per salvare lo stato dell'UI, consulta la documentazione generale Salvataggio dello stato dell'UI e la pagina del livello dati della guida all'architettura.