Salva stato UI in Compose

A seconda di dove è istruito lo stato e della logica richiesta, puoi utilizzare diverse API per archiviare e ripristinare lo stato della UI. Ogni app usa una combinazione di API per raggiungere questo obiettivo.

Qualsiasi app per Android potrebbe perdere lo stato dell'interfaccia utente a causa di attività o creazione di processi. Questa perdita di stato può verificarsi a causa dei seguenti eventi:

Conservare lo stato dopo questi eventi è essenziale per un'esperienza utente positiva. La selezione dello stato da mantenere dipende dai flussi di utenti unici della tua app. Come best practice, devi almeno preservare lo stato dell'input utente e della navigazione. Alcuni esempi sono la posizione di scorrimento di un elenco, l'ID dell'elemento su cui l'utente desidera maggiori dettagli, la selezione in corso delle preferenze dell'utente o l'inserimento nei campi di testo.

Questa pagina riassume le API disponibili per l'archiviazione dello stato dell'interfaccia utente in base a dove viene istruito lo stato e alla logica che ne ha bisogno.

Logica UI

Se il tuo stato è visualizzato nell'interfaccia utente, in funzioni componibili o classi di titolari di stato semplici con l'ambito della composizione, puoi utilizzare rememberSaveable per mantenere lo stato in tutte le attività e i processi di creazione.

Nello snippet seguente, rememberSaveable viene utilizzato per memorizzare un singolo stato dell'elemento 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. Il fumetto del messaggio di chat si espande e si comprime quando viene toccato.

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

rememberSaveable archivia 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 il tuo stato è di un tipo non primitivo, come una classe di dati, puoi utilizzare diversi meccanismi di archiviazione, ad esempio l'annotazione Parcelize, l'API Compose come listSaver e mapSaver o l'implementazione di una classe di salvaschermo personalizzata che estende la classe di runtime Saver di Compose. Per saperne di più su questi metodi, consulta la documentazione relativa alle modalità di archiviazione dello stato.

Nel seguente snippet, l'API rememberLazyListState Compose archivia LazyListState, ovvero lo stato di scorrimento di un LazyColumn o LazyRow utilizzando rememberSaveable. Utilizza LazyListState.Saver, ovvero un salvaschermo 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 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 usa un elemento Bundle per archiviare lo stato dell'interfaccia utente, che è condiviso da altre API che vi scrivono, ad esempio le chiamate onSaveInstanceState() nella tua attività. Tuttavia, la dimensione di questo Bundle è limitata e l'archiviazione di oggetti di grandi dimensioni potrebbe comportare eccezioni TransactionTooLarge in fase di runtime. Questo può essere particolarmente problematico in singole app Activity in cui lo stesso Bundle viene utilizzato nell'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, ad esempio ID o chiavi, e utilizzali per delegare il ripristino di stati dell'interfaccia utente più complessi ad altri meccanismi, come l'archiviazione permanente.

Le scelte di progettazione dipendono dai casi d'uso specifici della tua app e dal comportamento degli utenti.

Verifica il ripristino dello stato

Puoi verificare che lo stato archiviato con rememberSaveable negli elementi Scrivi venga ripristinato correttamente quando l'attività o il processo vengono creati di nuovo. Esistono API specifiche per raggiungere questo obiettivo, come StateRestorationTester. Per ulteriori informazioni, consulta la documentazione relativa ai test.

Logica di business

Se lo stato dell'elemento UI viene iscritta a ViewModel perché è richiesto dalla logica di business, puoi utilizzare le API di ViewModel.

Uno dei principali vantaggi di usare ViewModel in un'app Android è che gestisce senza costi le modifiche alla configurazione. Quando viene apportata una modifica alla configurazione e l'attività viene eliminata e ricreata, lo stato dell'UI sollevato in ViewModel viene conservato in memoria. Al termine della creazione, la vecchia istanza ViewModel viene collegata alla nuova istanza dell'attività.

Tuttavia, un'istanza ViewModel non sopravvive al decesso del processo avviato dal sistema. Affinché lo stato dell'interfaccia utente rimanga invariato, 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'interfaccia utente, quindi devi utilizzarlo solo per archiviare lo stato degli elementi UI semplice.

Lo stato dell'interfaccia utente della schermata, generato dall'applicazione di regole aziendali e dall'accesso a livelli dell'applicazione diversi dall'interfaccia utente, non deve essere archiviato in SavedStateHandle a causa delle sue potenziali complessità e dimensioni. Puoi utilizzare diversi meccanismi 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 archiviato in SavedStateHandle (se presente) e lo stato dell'interfaccia utente della schermata viene nuovamente prodotto dal livello dati.

SavedStateHandle API

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

Scrivi State saveable()
StateFlow getStateFlow()

Scrivi State

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

L'API saveable supporta immediatamente i tipi primitivi e riceve un parametro stateSaver per utilizzare i salvaschermo personalizzati, proprio come rememberSaveable().

Nel seguente snippet, message archivia i tipi di input utente 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 relativa a SavedStateHandle.

StateFlow

Utilizza getStateFlow() per archiviare lo stato degli elementi UI e utilizzarlo come un flusso da SavedStateHandle. 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 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 archiviato con la chiave _CHANNEL_FILTER_SAVED_STATE_KEY_. savedFilterType è un flusso che emette il valore più recente archiviato nella chiave. filteredChannels ha effettuato l'iscrizione al flusso per applicare i filtri del canale.

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

Riepilogo

La tabella seguente riassume le API trattate in questa sezione e quando utilizzarle per salvare lo stato dell'interfaccia utente:

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

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

Dovresti utilizzare le API bundle (rememberSaveable e SavedStateHandle) per archiviare piccole quantità di stato dell'interfaccia utente. Questi dati sono il minimo necessario per ripristinare lo stato precedente dell'interfaccia utente, insieme ad altri meccanismi di archiviazione. Ad esempio, se memorizzi l'ID di un profilo che l'utente stava esaminando 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'interfaccia utente, consulta la documentazione generale sul salvataggio dello stato dell'interfaccia utente e la pagina del livello dati della guida all'architettura.