Salvare gli stati della UI (visualizzazioni)

Concetti e implementazione di Jetpack Compose

Questa guida illustra le aspettative degli utenti in merito allo stato dell'UI e le opzioni disponibili per conservarlo.

Salvare e ripristinare rapidamente lo stato dell'UI di un'attività dopo che il sistema distrugge le attività o le applicazioni è essenziale per una buona esperienza utente. Gli utenti si aspettano che lo stato dell'UI rimanga invariato, ma il sistema potrebbe eliminare l'attività e il relativo stato memorizzato.

Per colmare il divario tra le aspettative degli utenti e il comportamento del sistema, utilizza una combinazione dei seguenti metodi:

  • ViewModel oggetti.
  • Stati dell'istanza salvati nei seguenti contesti:
  • Spazio di archiviazione locale per rendere persistente lo stato dell'UI durante le transizioni di app e attività.

La soluzione ottimale dipende dalla complessità dei dati dell'UI, dai casi d'uso dell'app e dalla ricerca di un equilibrio tra velocità di accesso ai dati e utilizzo della memoria.

Assicurati che la tua app soddisfi le aspettative degli utenti e offra un'interfaccia veloce e reattiva. Evita ritardi durante il caricamento dei dati nell'UI, in particolare dopo modifiche comuni alla configurazione come la rotazione.

Aspettative degli utenti e comportamento del sistema

A seconda dell'azione intrapresa da un utente, si aspetta che lo stato dell'attività venga cancellato o conservato. In alcuni casi, il sistema esegue automaticamente ciò che l'utente si aspetta. In altri casi, il sistema fa il contrario.

Ignorare lo stato dell'UI avviato dall'utente

L'utente si aspetta che, quando avvia un'attività, lo stato dell'UI temporaneo di questa attività rimanga invariato finché l'utente non la ignora completamente. L'utente può ignorare completamente un'attività nei seguenti modi:

  • Scorrendo l'attività dalla schermata Panoramica (Recenti).
  • Terminando o forzando l'uscita dall'app dalla schermata Impostazioni.
  • Riavviando il dispositivo.
  • Completando un'azione di "fine" (supportata da Activity.finish()).

In questi casi di ignoramento completo, l'utente presuppone di aver abbandonato definitivamente l'attività e, se la riapre, si aspetta che l'attività venga avviata da uno stato pulito. Il comportamento del sistema sottostante per questi scenari di ignoramento corrisponde all'aspettativa dell'utente: l'istanza dell'attività verrà distrutta e rimossa dalla memoria, insieme a qualsiasi stato memorizzato e a qualsiasi record di stato dell'istanza salvato associato all'attività.

Esistono alcune eccezioni a questa regola sull'ignoramento completo. Ad esempio, un utente potrebbe aspettarsi che un browser lo porti alla pagina web esatta che stava visualizzando prima di uscire dal browser utilizzando il pulsante Indietro.

Ignorare lo stato dell'UI avviato dal sistema

Un utente si aspetta che lo stato dell'UI di un'attività rimanga invariato durante una modifica della configurazione, ad esempio la rotazione o il passaggio alla modalità multi-finestra. Tuttavia, per impostazione predefinita, il sistema distrugge l'attività quando si verifica una modifica della configurazione, eliminando qualsiasi stato dell'UI memorizzato nell'istanza dell'attività. Per scoprire di più sulle configurazioni dei dispositivi, consulta la pagina di riferimento della configurazione.

Tieni presente che è possibile (anche se non è consigliabile) sostituire il comportamento predefinito per le modifiche alla configurazione. Per maggiori dettagli, consulta Gestire la modifica della configurazione.

Un utente si aspetta anche che lo stato dell'UI della tua attività rimanga invariato se passa temporaneamente a un'altra app e poi torna alla tua app in un secondo momento. Ad esempio, l'utente esegue una ricerca nella tua attività di ricerca, quindi preme il pulsante Home o risponde a una chiamata. Quando torna all'attività di ricerca, si aspetta di trovare la parola chiave per la rete di ricerca e i risultati esattamente come prima.

In questo scenario, la tua app viene inserita in background e il sistema fa del suo meglio per mantenere il processo dell'app in memoria. Tuttavia, il sistema potrebbe eliminare il processo dell'applicazione mentre l'utente è impegnato a interagire con altre app. In questo caso, l'istanza dell'attività viene eliminata, insieme a qualsiasi stato memorizzato. Quando l'utente riavvia l'app, l'attività si trova in uno stato pulito in modo imprevisto. Per scoprire di più sull'interruzione del processo, consulta Ciclo di vita dei processi e delle app.

Opzioni per conservare lo stato dell'UI

Quando le aspettative dell'utente in merito allo stato dell'UI non corrispondono al comportamento predefinito del sistema, devi salvare e ripristinare lo stato dell'UI dell'utente per assicurarti che la distruzione avviata dal sistema sia trasparente per l'utente.

Ognuna delle opzioni per conservare lo stato dell'UI varia in base alle seguenti dimensioni che influiscono sull'esperienza utente:

ViewModel

Stato dell'istanza salvato

Spazio di archiviazione permanente

Località di archiviazione

in memoria

in memoria

su disco o rete

Sopravvive alla modifica della configurazione

Sopravvive all'interruzione del processo avviata dal sistema

No

Sopravvive all'ignoramento/alla fine completa dell'attività dell'utente (finish())

No

No

Limitazioni dei dati

gli oggetti complessi vanno bene, ma lo spazio è limitato dalla memoria disponibile

solo per tipi primitivi e oggetti semplici e piccoli come String

limitato solo dallo spazio su disco o dal costo / tempo di recupero dalla risorsa di rete

Tempo di lettura/scrittura

veloce (solo accesso alla memoria)

lento (richiede serializzazione/deserializzazione)

lento (richiede accesso al disco o transazione di rete)

Utilizzare ViewModel per gestire le modifiche alla configurazione

ViewModel è ideale per archiviare e gestire i dati relativi all'UI mentre l'utente utilizza attivamente l'applicazione. Consente un accesso rapido ai dati dell'UI e ti aiuta a evitare di recuperare i dati dalla rete o dal disco durante la rotazione, il ridimensionamento della finestra e altre modifiche alla configurazione che si verificano di frequente. Per scoprire come implementare un ViewModel, consulta la guida di ViewModel.

ViewModel conserva i dati in memoria, il che significa che il recupero è più economico rispetto ai dati del disco o della rete. Un ViewModel è associato a un'attività (o a un altro proprietario del ciclo di vita): rimane in memoria durante una modifica della configurazione e il sistema associa automaticamente il ViewModel alla nuova istanza dell'attività risultante dalla modifica della configurazione.

I ViewModel vengono distrutti automaticamente dal sistema quando l'utente torna indietro dall'attività o dal fragment oppure se chiami finish(), il che significa che lo stato viene cancellato come previsto dall'utente in questi scenari.

A differenza dello stato dell'istanza salvato, i ViewModel vengono distrutti durante un'interruzione del processo avviata dal sistema. Per ricaricare i dati dopo un'interruzione del processo avviata dal sistema in un ViewModel, utilizza l'SavedStateHandle API. In alternativa, se i dati sono correlati all'UI e non devono essere mantenuti nel ViewModel, utilizza onSaveInstanceState(). Se i dati sono dati dell'applicazione, potrebbe essere preferibile renderli persistenti sul disco.

Se hai già una soluzione in memoria per archiviare lo stato dell'UI durante le modifiche alla configurazione, potresti non dover utilizzare ViewModel.

Utilizzare lo stato dell'istanza salvato come backup per gestire l'interruzione del processo avviata dal sistema

Il onSaveInstanceState() callback nel sistema di visualizzazione e SavedStateHandle in ViewModel memorizzano i dati necessari per ricaricare lo stato di un controller dell'UI, ad esempio un'attività o un fragment, se il sistema distrugge e poi ricrea il controller. Per scoprire come implementare lo stato dell'istanza salvato utilizzando onSaveInstanceState, consulta Salvare e ripristinare lo stato dell'attività nella guida al ciclo di vita dell'attività.

I bundle di stato dell'istanza salvati persistono sia durante le modifiche alla configurazione sia durante l'interruzione del processo, ma sono limitati dallo spazio di archiviazione e dalla velocità, perché le diverse API serializzano i dati. La serializzazione può consumare molta memoria se gli oggetti serializzati sono complessi. Poiché questo processo viene eseguito sul thread principale durante una modifica della configurazione, una serializzazione a lunga esecuzione può causare frame persi e stuttering visivo.

Non utilizzare lo stato dell'istanza salvato per archiviare grandi quantità di dati, ad esempio bitmap, né strutture di dati complesse che richiedono una serializzazione o deserializzazione lunga. Memorizza invece solo tipi primitivi e oggetti semplici e piccoli come String. Pertanto, utilizza lo stato dell'istanza salvato per archiviare una quantità minima di dati necessari, ad esempio un ID, per ricreare i dati necessari per ripristinare l'UI al suo stato precedente in caso di errore degli altri meccanismi di persistenza. La maggior parte delle app dovrebbe implementare questa funzionalità per gestire l'interruzione del processo avviata dal sistema.

A seconda dei casi d'uso dell'app, potresti non dover utilizzare affatto lo stato dell'istanza salvato. Ad esempio, un browser potrebbe riportare l'utente alla pagina web esatta che stava visualizzando prima di uscire dal browser. Se la tua attività si comporta in questo modo, puoi evitare di utilizzare lo stato dell'istanza salvato e rendere persistente tutto localmente.

Inoltre, quando apri un'attività da un intent, il bundle di extra viene fornito all'attività sia quando la configurazione cambia sia quando il sistema ripristina l'attività.

In entrambi questi scenari, devi comunque utilizzare un ViewModel per evitare di sprecare cicli di ricaricamento dei dati dal database durante una modifica della configurazione.

Nei casi in cui i dati dell'UI da conservare sono semplici e leggeri, puoi utilizzare solo le API dello stato dell'istanza salvato per conservare i dati di stato.

Agganciare lo stato salvato utilizzando SavedStateRegistry

A partire da Fragment 1.1.0 o dalla relativa dipendenza transitiva Activity 1.0.0, i controller dell'UI, ad esempio un Activity o un Fragment, implementano SavedStateRegistryOwner e forniscono un SavedStateRegistry associato a quel controller. SavedStateRegistry consente ai componenti di agganciare lo stato salvato del controller dell'UI per utilizzarlo o contribuirvi. Ad esempio, il modulo Stato salvato per ViewModel utilizza SavedStateRegistry per creare un SavedStateHandle e fornirlo agli oggetti ViewModel. Puoi recuperare il SavedStateRegistry dal controller dell'UI chiamando getSavedStateRegistry.

I componenti che contribuiscono allo stato salvato devono implementare SavedStateRegistry.SavedStateProvider, che definisce un singolo metodo chiamato saveState. Il metodo saveState() consente al componente di restituire un Bundle contenente qualsiasi stato che deve essere salvato dal componente. SavedStateRegistry chiama questo metodo durante la fase di salvataggio dello stato del ciclo di vita del controller dell'UI.

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;
    }
}

Per registrare un SavedStateProvider, chiama registerSavedStateProvider() su il SavedStateRegistry, passando una chiave da associare ai dati del provider e al provider. I dati salvati in precedenza per il provider possono essere recuperati dallo stato salvato chiamando consumeRestoredStateForKey() su SavedStateRegistry, passando la chiave associata ai dati del provider.

All'interno di un Activity o Fragment, puoi registrare un SavedStateProvider in onCreate() dopo aver chiamato super.onCreate(). In alternativa, puoi impostare un LifecycleObserver su un SavedStateRegistryOwner, che implementa LifecycleOwner, e registrare SavedStateProvider quando si verifica l'evento ON_CREATE. Utilizzando un LifecycleObserver, puoi disaccoppiare la registrazione e il recupero dello stato salvato in precedenza da SavedStateRegistryOwner stesso.

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);
    ...
}

Utilizzare la persistenza locale per gestire l'interruzione del processo per dati complessi o di grandi dimensioni

Lo spazio di archiviazione locale permanente, ad esempio un database o le preferenze condivise, sopravvive finché l'applicazione è installata sul dispositivo dell'utente (a meno che l'utente non cancelli i dati dell'app). Sebbene questo spazio di archiviazione locale sopravviva all'interruzione del processo dell'attività e dell'applicazione avviata dal sistema, il recupero può essere costoso perché dovrà essere letto dallo spazio di archiviazione locale in memoria. Spesso questo spazio di archiviazione locale permanente potrebbe già far parte dell'architettura dell'applicazione per archiviare tutti i dati che non vuoi perdere se apri e chiudi l'Activity.

Né ViewModel né lo stato dell'istanza salvato sono soluzioni di archiviazione a lungo termine e pertanto non sostituiscono lo spazio di archiviazione locale, ad esempio un database. Devi invece utilizzare questi meccanismi solo per archiviare temporaneamente lo stato dell'UI temporaneo e utilizzare lo spazio di archiviazione permanente per altri dati dell'app. Per maggiori dettagli su come sfruttare lo spazio di archiviazione locale per rendere persistenti i dati del modello dell'app a lungo termine (ad es. durante i riavvii del dispositivo), consulta la Guida all'architettura delle app .

Gestire lo stato dell'UI: dividi et impera

Puoi salvare e ripristinare in modo efficiente lo stato dell'UI dividendo il lavoro tra i vari tipi di meccanismi di persistenza. Nella maggior parte dei casi, ognuno di questi meccanismi deve archiviare un tipo diverso di dati utilizzati nell'attività, in base ai compromessi tra complessità dei dati, velocità di accesso e durata:

  • Persistenza locale: archivia tutti i dati dell'applicazione che non vuoi perdere se apri e chiudi l'attività.
    • Esempio: una raccolta di oggetti di brani, che potrebbero includere file audio e metadati.
  • ViewModel: archivia in memoria tutti i dati necessari per visualizzare l' UI associata, lo stato dell'UI dello schermo.
    • Esempio: gli oggetti di brani della ricerca più recente e la query di ricerca più recente.
  • Stato dell'istanza salvato: archivia una piccola quantità di dati necessari per ricaricare lo stato dell'UI se il sistema arresta e poi ricrea l'UI. Anziché archiviare oggetti complessi qui, rendi persistenti gli oggetti complessi nello spazio di archiviazione locale e archivia un ID univoco per questi oggetti nelle API dello stato dell'istanza salvato.
    • Esempio: memorizzazione della query di ricerca più recente.

Ad esempio, considera un'attività che ti consente di cercare nella tua libreria di brani. Ecco come gestire i diversi eventi:

Quando l'utente aggiunge un brano, il ViewModel delega immediatamente la persistenza di questi dati localmente. Se questo brano appena aggiunto deve essere mostrato nell'UI, devi anche aggiornare i dati nell'oggetto ViewModel per riflettere l'aggiunta del brano. Ricorda di eseguire tutti gli inserimenti nel database al di fuori del thread principale.

Quando l'utente cerca un brano, tutti i dati complessi del brano caricati dal database devono essere immediatamente archiviati nell'oggetto ViewModel come parte dello stato dell'UI dello schermo.

Quando l'attività passa in background e il sistema chiama le API dello stato dell'istanza salvato, la query di ricerca deve essere archiviata nello stato dell'istanza salvato, nel caso in cui il processo venga ricreato. Poiché le informazioni sono necessarie per caricare i dati dell'applicazione resi persistenti in questo, archivia la query di ricerca in ViewModel SavedStateHandle. Queste sono tutte le informazioni necessarie per caricare i dati e riportare l'UI al suo stato attuale.

Ripristinare stati complessi: riassemblare i pezzi

Quando è il momento per l'utente di tornare all'attività, esistono due possibili scenari per ricreare l'attività:

  • L'attività viene ricreata dopo essere stata arrestata dal sistema. Il sistema ha la query salvata in un bundle di stato dell'istanza salvato e l'UI deve passare la query a ViewModel se SavedStateHandle non viene utilizzato. ViewModel vede che non ha risultati di ricerca memorizzati nella cache e delega il caricamento dei risultati di ricerca utilizzando la query di ricerca specificata.
  • L'attività viene ricreata dopo una modifica della configurazione. Poiché l'istanza ViewModel non è stata distrutta, ViewModel ha tutte le informazioni memorizzate nella cache in memoria e non deve eseguire di nuovo una query sul database.

Risorse aggiuntive

Per scoprire di più sul salvataggio degli stati dell'UI, consulta le seguenti risorse.

Blog

Codelab