Salvare gli stati della UI (visualizzazioni)

Concetti e implementazione di Jetpack Compose

Questa guida illustra le aspettative degli utenti sullo stato dell'interfaccia utente e le opzioni disponibili per preservare lo stato.

Salvare e ripristinare rapidamente lo stato dell'interfaccia utente di un'attività dopo che il sistema distrugge attività o applicazioni è essenziale per una buona esperienza utente. Gli utenti si aspettano che lo stato della 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:

  • Oggetti ViewModel.
  • Stati delle istanze salvati nei seguenti contesti:
  • Archiviazione locale per mantenere lo stato dell'interfaccia utente durante le transizioni tra app e attività.

La soluzione ottimale dipende dalla complessità dei dati dell'interfaccia utente, dai casi d'uso dell'app e dal raggiungimento 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'interfaccia utente, in particolare dopo modifiche comuni alla configurazione come la rotazione.

Aspettative degli utenti e comportamento del sistema

A seconda dell'azione intrapresa, l'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.

Chiusura dello stato dell'interfaccia utente avviata dall'utente

L'utente si aspetta che quando avvia un'attività, lo stato dell'interfaccia utente temporanea di quell'attività rimanga invariato finché non la chiude completamente. L'utente può chiudere completamente un'attività nel seguente modo:

  • Scorrendo l'attività fuori dalla schermata Panoramica (Recenti).
  • Chiudere o forzare l'uscita dall'app dalla schermata Impostazioni.
  • Riavvio del dispositivo.
  • Completamento di un'azione di "completamento" (supportata da Activity.finish()).

L'utente presume che in questi casi di chiusura completa abbia abbandonato definitivamente l'attività e che, se la riapre, si aspetta che l'attività inizi da uno stato pulito. Il comportamento del sistema sottostante per questi scenari di chiusura corrisponde alle aspettative dell'utente: l'istanza dell'attività verrà eliminata 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'abbandono completo. Ad esempio, un utente potrebbe aspettarsi che un browser lo indirizzi alla pagina web esatta che stava visualizzando prima di uscire dal browser utilizzando il pulsante Indietro.

Chiusura dello stato dell'interfaccia utente avviata dal sistema

Un utente si aspetta che lo stato dell'interfaccia utente 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'interfaccia utente memorizzato nell'istanza dell'attività. Per scoprire di più sulle configurazioni dei dispositivi, consulta la pagina di riferimento alla configurazione.

Tieni presente che è possibile (anche se non consigliato) ignorare il comportamento predefinito per le modifiche alla configurazione. Per ulteriori dettagli, consulta Gestione della modifica della configurazione.

Un utente si aspetta anche che lo stato dell'interfaccia utente della tua attività rimanga lo stesso 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 e poi preme il pulsante Home o risponde a una chiamata. Quando torna all'attività di ricerca, si aspetta di trovare ancora la parola chiave per la rete di ricerca e i risultati, esattamente come prima.

In questo scenario, l'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. Per scoprire di più sull'interruzione del processo, consulta Processi e ciclo di vita delle app.

Opzioni per mantenere lo stato dell'interfaccia utente

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

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

ViewModel

Stato dell'istanza salvato

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 alla chiusura/al completamento dell'attività utente

No

No

Limitazioni per i 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

rapido (solo accesso alla memoria)

lento (richiede serializzazione/deserializzazione)

lento (richiede l'accesso al disco o una transazione di rete)

Utilizzare ViewModel per gestire le modifiche alla configurazione

ViewModel è ideale per archiviare e gestire i dati correlati all'interfaccia utente mentre l'utente utilizza attivamente l'applicazione. Consente l'accesso rapido ai dati dell'interfaccia utente 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 a ViewModel.

ViewModel mantiene i dati in memoria, il che significa che il recupero è più economico rispetto ai dati dal disco o dalla 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 eliminati automaticamente dal sistema quando l'utente esce 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 eliminati durante l'interruzione del processo avviata dal sistema. Per ricaricare i dati dopo l'interruzione di un processo avviato dal sistema in un ViewModel, utilizza l'SavedStateHandle API. In alternativa, se i dati sono correlati alla UI e non devono essere conservati nel ViewModel, utilizza onSaveInstanceState(). Se i dati sono dati dell'applicazione, potrebbe essere meglio salvarli su disco.

Se hai già una soluzione in memoria per archiviare lo stato della UI in seguito a 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 callback onSaveInstanceState() nel sistema View e SavedStateHandle in ViewModels memorizza i dati necessari per ricaricare lo stato di un controller UI, ad esempio un'attività o un fragment, se il sistema distrugge e ricrea successivamente il controller. Per scoprire come implementare lo stato dell'istanza salvata utilizzando onSaveInstanceState, consulta Salvataggio e ripristino dello stato dell'attività nella guida al ciclo di vita dell'attività.

I bundle dello stato dell'istanza salvati vengono mantenuti sia in caso di modifiche alla configurazione sia in caso di interruzione del processo, ma sono limitati da spazio di archiviazione e velocità, perché le diverse API serializzano i dati. La serializzazione può consumare molta memoria se gli oggetti serializzati sono complessi. Poiché questo processo si verifica sul thread principale durante una modifica della configurazione, la serializzazione a esecuzione prolungata può causare la perdita di frame e stuttering visivo.

Non utilizzare lo stato dell'istanza salvato per archiviare grandi quantità di dati, come bitmap, né strutture di dati complesse che richiedono una serializzazione o deserializzazione lunga. Memorizza 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 la UI al suo stato precedente in caso di errore degli altri meccanismi di persistenza. La maggior parte delle app deve implementare questa funzionalità per gestire l'interruzione del processo avviata dal sistema.

A seconda dei casi d'uso della tua app, potresti non dover utilizzare affatto lo stato dell'istanza salvata. Ad esempio, un browser potrebbe riportare l'utente esattamente alla pagina web che stava visualizzando prima di uscire dal browser. Se la tua attività si comporta in questo modo, puoi rinunciare all'utilizzo dello stato dell'istanza salvato e persistere tutto in locale.

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 gli scenari, devi comunque utilizzare un ViewModel per evitare di sprecare cicli ricaricando i dati dal database durante una modifica alla configurazione.

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

Utilizzare lo stato salvato con SavedStateRegistry

A partire da Fragment 1.1.0 o dalla relativa dipendenza transitiva Activity 1.0.0, i controller UI, come Activity o Fragment, implementano SavedStateRegistryOwner e forniscono un SavedStateRegistry associato a quel controller. SavedStateRegistry consente ai componenti di collegarsi allo stato salvato del controller UI per utilizzarlo o contribuirvi. Ad esempio, il modulo Saved State per ViewModel utilizza SavedStateRegistry per creare un SavedStateHandle e fornirlo agli oggetti ViewModel. Puoi recuperare il SavedStateRegistry dal controller 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 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() sul SavedStateRegistry, passando una chiave da associare ai dati del fornitore e al fornitore. 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 il numero super.onCreate(). In alternativa, puoi impostare un LifecycleObserver su un SavedStateRegistryOwner, che implementa LifecycleOwner, e registrare SavedStateProvider una volta che si verifica l'evento ON_CREATE. Utilizzando un LifecycleObserver, puoi disaccoppiare la registrazione e il recupero dello stato salvato in precedenza dal 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

L'archiviazione locale persistente, ad esempio un database o preferenze condivise, rimarrà 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'attività avviata dal sistema e all'interruzione del processo dell'applicazione, il recupero può essere costoso perché dovrà essere letto dallo spazio di archiviazione locale nella memoria. Spesso questo spazio di archiviazione locale persistente potrebbe già far parte dell'architettura dell'applicazione per archiviare tutti i dati che non vuoi perdere se apri e chiudi l'attività.

Né ViewModel né lo stato dell'istanza salvata sono soluzioni di archiviazione a lungo termine e pertanto non sostituiscono l'archiviazione locale, ad esempio un database. Invece, devi utilizzare questi meccanismi solo per archiviare temporaneamente lo stato dell'interfaccia utente temporaneo e utilizzare l'archiviazione permanente per altri dati dell'app. Per maggiori dettagli su come sfruttare l'archiviazione locale per conservare i dati del modello dell'app a lungo termine (ad es. dopo i riavvii del dispositivo), consulta la Guida all'architettura delle app.

Gestione dello stato dell'interfaccia utente: dividi e conquista

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

  • Persistenza locale: memorizza tutti i dati dell'applicazione che non vuoi perdere se apri e chiudi l'attività.
    • Esempio: una raccolta di oggetti brano, che potrebbe includere file audio e metadati.
  • ViewModel: memorizza tutti i dati necessari per visualizzare l'interfaccia utente associata, lo stato dell'interfaccia utente della schermata.
    • Esempio: gli oggetti brano dell'ultima ricerca e l'ultima query di ricerca.
  • Stato dell'istanza salvato: memorizza una piccola quantità di dati necessari per ricaricare lo stato dell'interfaccia utente se il sistema si arresta e poi ricrea l'interfaccia utente. Anziché archiviare oggetti complessi qui, mantieni gli oggetti complessi nell'archiviazione locale e memorizza un ID univoco per questi oggetti nelle API di stato dell'istanza salvata.
    • 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 devono essere gestiti i diversi eventi:

Quando l'utente aggiunge un brano, ViewModel delega immediatamente la persistenza di questi dati a livello locale. Se il brano appena aggiunto deve essere mostrato nell'interfaccia utente, devi aggiornare anche i dati nell'oggetto ViewModel per riflettere l'aggiunta del brano. Ricordati 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'interfaccia utente della schermata.

Quando l'attività passa in background e il sistema chiama le API di stato dell'istanza salvata, la query di ricerca deve essere archiviata nello stato dell'istanza salvata, nel caso in cui il processo venga ricreato. Poiché le informazioni sono necessarie per caricare i dati dell'applicazione archiviati in questo modo, memorizza la query di ricerca nel ViewModel SavedStateHandle. Queste sono tutte le informazioni necessarie per caricare i dati e ripristinare lo stato attuale della UI.

Ripristinare stati complessi: ricomporre i pezzi

Quando è il momento per l'utente di tornare all'attività, ci sono due possibili scenari per ricrearla:

  • L'attività viene ricreata dopo essere stata interrotta dal sistema. Il sistema ha salvato la query in un bundle di stato dell'istanza salvato e l'interfaccia utente 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 fornita.
  • L'attività viene ricreata dopo una modifica alla configurazione. Poiché l'istanza ViewModel non è stata eliminata, ViewModel ha tutte le informazioni memorizzate nella cache e non deve eseguire nuovamente query sul database.

Risorse aggiuntive

Per saperne di più sul salvataggio degli stati dell'interfaccia utente, consulta le seguenti risorse.

Blog

Codelab