Salva stati UI

Questa guida illustra le aspettative degli utenti in merito allo stato dell'interfaccia utente e alle opzioni disponibili per mantenere lo stato.

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

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

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

Assicurati che l'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 eseguita, l'utente si aspetta che lo stato dell'attività venga cancellato o che lo stato venga conservato. In alcuni casi, il sistema fa automaticamente ciò che si aspetta dall'utente. In altri casi, il sistema fa il contrario di ciò che si aspetta l'utente.

Chiusura dello stato dell'interfaccia utente avviata dall'utente

L'utente si aspetta che quando avvia un'attività, lo stato temporaneo dell'interfaccia utente dell'attività rimanga lo stesso finché l'utente non la ignora completamente. L'utente può ignorare completamente un'attività procedendo nel seguente modo:

  • Scorrimento dell'attività dalla schermata Panoramica (Recenti).
  • Chiusura o chiusura forzata dell'app dalla schermata Impostazioni.
  • Riavvio del dispositivo.
  • Completamento dell'azione di "fine" (supportata da Activity.finish()).

In questi casi di chiusura completi, l'utente parte dal presupposto che l'utente è uscito definitivamente dall'attività e, se riapre l'attività, si aspetta che inizi dallo stato originale. Il comportamento di base del sistema per questi scenari di chiusura corrisponde alle aspettative dell'utente: l'istanza dell'attività verrà eliminata e rimossa dalla memoria, insieme allo stato archiviato al suo interno e a qualsiasi record di stato dell'istanza salvato associato all'attività.

Esistono alcune eccezioni a questa regola relativa all'eliminazione completa, ad esempio un utente potrebbe aspettarsi che un browser lo indirizzi esattamente alla pagina web che stava visitando 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 della UI di un'attività rimanga lo stesso durante una modifica alla configurazione, ad esempio la rotazione o il passaggio alla modalità multi-finestra. Tuttavia, per impostazione predefinita, il sistema elimina l'attività quando si verifica una modifica della configurazione, cancellando qualsiasi stato dell'interfaccia utente archiviato nell'istanza dell'attività. Per scoprire di più sulle configurazioni dei dispositivi, consulta la pagina di riferimento per la configurazione. Tieni presente che è possibile (anche se non è consigliabile) eseguire l'override del comportamento predefinito per le modifiche alla configurazione. Per ulteriori dettagli, consulta Gestire le modifiche alla configurazione in autonomia.

Un utente si aspetta anche che lo stato UI 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 telefonata. Quando torna all'attività di ricerca, si aspetta di trovare la parola chiave di ricerca e i risultati ancora lì, esattamente come prima.

In questo scenario, la tua app viene posizionata in background, in cui il sistema fa del suo meglio per mantenere il processo dell'app in memoria. Tuttavia, il sistema potrebbe distruggere il processo di applicazione mentre l'utente non è in grado di interagire con altre app. In questo caso, l'istanza dell'attività viene eliminata insieme a qualsiasi stato archiviato al suo interno. Quando l'utente riavvia l'app, l'attività si trova inaspettatamente in uno stato pulito. Per scoprire di più sulla morte dei processi, consulta Processi e ciclo di vita delle applicazioni.

Opzioni per la conservazione dello stato dell'UI

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 per assicurarti che l'eliminazione avviata dal sistema sia trasparente per l'utente.

Ognuna delle opzioni per la conservazione dello stato dell'interfaccia utente varia in base alle seguenti dimensioni che influiscono sull'esperienza utente:

Visualizza modello Stato dell'istanza salvato Archiviazione permanente
Posizione archiviazione in memoria in memoria su disco o rete
Resiste alla modifica della configurazione
Sopravvive al processo avviato dal sistema No
Sopravvive al completamento dell'attività dell'utente, eliminazione/onFinish() No No
Limitazioni dei dati oggetti complessi sono consentiti, ma lo spazio è limitato dalla memoria disponibile solo per i tipi primitivi e gli oggetti semplici e piccoli come String Limitata 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 relativi all'interfaccia utente mentre l'utente utilizza attivamente l'applicazione. Consente di accedere rapidamente ai dati dell'interfaccia utente e ti aiuta a evitare il recupero dei dati dalla rete o dal disco durante la rotazione, il ridimensionamento delle finestre e altre modifiche comuni alla configurazione. Per informazioni su come implementare un ViewModel, consulta la guida ViewModel.

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

I modelli ViewModel vengono automaticamente eliminati dal sistema quando l'utente esce dall'attività o dal frammento o se chiami finish(), il che significa che lo stato viene cancellato come l'utente si aspetta in questi scenari.

A differenza dello stato dell'istanza salvata, i modelli ViewModel vengono eliminati durante l'interruzione del processo avviato dal sistema. Per ricaricare i dati dopo un decesso di processo avviato dal sistema in un ViewModel, utilizza l'API SavedStateHandle. In alternativa, se i dati sono correlati all'interfaccia utente e non devono essere archiviati nel ViewModel, utilizza onSaveInstanceState() nel sistema View o rememberSaveable in Jetpack Compose. Se i dati sono dati dell'applicazione, potrebbe essere meglio conservarli su disco.

Se disponi già di una soluzione in memoria per l'archiviazione dello stato dell'UI attraverso le modifiche alla configurazione, potrebbe non essere necessario utilizzare ViewModel.

Usa lo stato dell'istanza salvata come backup per gestire la morte dei processi avviati dal sistema

Il callback onSaveInstanceState() nel sistema View, rememberSaveable in Jetpack Compose e SavedStateHandle in ViewModels archiviano i dati necessari per ricaricare lo stato di un controller UI, ad esempio un'attività o un frammento, se il sistema elimina e in seguito ricrea 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 delle attività.

I bundle di stato delle istanze salvati vengono mantenuti sia dopo le modifiche alla configurazione sia in caso di decesso dei processi, ma sono limitati dalla capacità di archiviazione e dalla velocità, perché le diverse API pubblicano i dati in serie. La serializzazione può consumare molta memoria se gli oggetti in serie sono complicati. Poiché questo processo avviene sul thread principale durante una modifica della configurazione, la serializzazione a lunga esecuzione può causare interruzioni dei frame e interruzioni visive.

Non utilizzare lo stato dell'istanza salvato per archiviare grandi quantità di dati, ad esempio bitmap, né strutture di dati complesse che richiedono lunghe serie di serializzazione o deserializzazione. Archivia solo i tipi primitivi e gli oggetti semplici e piccoli come String. Di conseguenza, utilizza lo stato dell'istanza salvata per archiviare una quantità minima di dati necessari, ad esempio un ID, per ricreare i dati necessari per ripristinare l'interfaccia utente allo stato precedente in caso di errore degli altri meccanismi di persistenza. La maggior parte delle app dovrebbe implementarla per gestire la morte dei processi avviati dal sistema.

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

Inoltre, quando apri un'attività da un intent, il pacchetto di extra viene pubblicato all'attività sia quando la configurazione viene modificata sia quando il sistema ripristina l'attività. Se un dato sullo stato dell'interfaccia utente, ad esempio una query di ricerca, è stato trasmesso come intent extra al momento dell'avvio dell'attività, potresti utilizzare il bundle extra anziché il pacchetto dello stato dell'istanza salvata. Per scoprire di più sulle opzioni aggiuntive per intent, consulta Filtri per intent e intent.

In uno di questi scenari, devi comunque utilizzare un elemento ViewModel per evitare di sprecare cicli che ricaricano 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 le sole API relative allo stato dell'istanza salvata per conservare i dati sullo stato.

Aggancia allo stato salvato utilizzando savedStateRegistry

A partire dal Fragment 1.1.0 o dalla sua 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 agganciarsi allo stato salvato del controller UI per utilizzarlo o contribuire. Ad esempio, il modulo Stato salvato per ViewModel utilizza SavedStateRegistry per creare un SavedStateHandle e fornirlo agli oggetti ViewModel. Puoi recuperare SavedStateRegistry dall'interno del controller UI chiamando getSavedStateRegistry().

I componenti che contribuiscono allo stato salvato devono implementare SavedStateRegistry.SavedStateProvider, che definisce un singolo metodo denominato 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.

Kotlin

class SearchManager : SavedStateRegistry.SavedStateProvider {
    companion object {
        private const val QUERY = "query"
    }

    private val query: String? = null

    ...

    override fun saveState(): Bundle {
        return bundleOf(QUERY to query)
    }
}

Java

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 provider e del provider. I dati salvati in precedenza per il provider possono essere recuperati dallo stato salvato chiamando consumeRestoredStateForKey() sul SavedStateRegistry, passando la chiave associata ai dati del provider.

All'interno di 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 il SavedStateProvider una volta che si verifica l'eventoON_CREATE. Se utilizzi un oggetto LifecycleObserver, puoi scollegare la registrazione e il recupero dello stato salvato in precedenza dall'elemento 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);
    ...
}

Utilizza la persistenza locale per gestire la morte dei processi per dati complessi o di grandi dimensioni

L'archiviazione locale permanente, ad esempio un database o preferenze condivise, sopravviverà finché l'applicazione sarà 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 al decesso del processo di applicazione, può essere costoso recuperarlo perché dovrà essere letto dallo spazio di archiviazione locale alla memoria. Spesso questa archiviazione locale permanente potrebbe già far parte dell'architettura dell'applicazione per archiviare tutti i dati da non perdere se apri e chiudi l'attività.

Né ViewModel né lo stato dell'istanza salvata sono soluzioni di archiviazione a lungo termine e quindi non sostituiscono l'archiviazione locale, ad esempio un database. Dovresti utilizzare questi meccanismi per archiviare temporaneamente solo lo stato temporaneo dell'interfaccia utente e utilizzare l'archiviazione permanente per altri dati dell'app. Consulta la guida all'architettura delle app per ulteriori dettagli su come utilizzare lo spazio di archiviazione locale per rendere persistenti i dati dei modelli di app a lungo termine (ad esempio tra riavvii del dispositivo).

Gestione dello stato dell'UI: 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, ognuno di questi meccanismi dovrebbe archiviare un tipo diverso di dati utilizzati nell'attività, in base ai compromessi tra complessità dei dati, velocità di accesso e durata:

  • Persistenza locale: memorizza tutti i dati dell'applicazione che non desideri perdere se apri e chiudi l'attività.
    • Esempio: una raccolta di oggetti brano, che potrebbe includere file audio e metadati.
  • ViewModel: memorizza in memoria tutti i dati necessari per visualizzare l'interfaccia utente associata, lo stato dell'interfaccia utente sullo schermo.
    • Esempio: gli oggetti brano della ricerca più recente e la query di ricerca più recente.
  • Stato istanza salvata: 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 qui gli oggetti complessi, memorizza gli oggetti complessi nello spazio di archiviazione locale e archivia un ID univoco per questi oggetti nelle API dello stato dell'istanza salvata.
    • Esempio: memorizzazione della query di ricerca più recente.

Prendiamo ad esempio un'attività che ti consente di cercare nella tua raccolta di brani. Ecco come devono essere gestiti i diversi eventi:

Quando l'utente aggiunge un brano, la ViewModel delega immediatamente la memorizzazione dei dati localmente. Se il brano appena aggiunto deve essere visualizzato nell'interfaccia utente, devi aggiornare anche i dati nell'oggetto ViewModel per riflettere l'aggiunta del brano. Ricordati di fare tutti gli inserti del database fuori dal thread principale.

Quando l'utente cerca un brano, qualunque sia i dati complessi del brano caricati dal database, il brano dovrebbe essere immediatamente archiviato nell'oggetto ViewModel come parte dello stato dell'interfaccia utente della schermata.

Quando l'attività passa in background e il sistema chiama le API dello 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 persistenti in questo, memorizza la query di ricerca in ViewModel SavedStateHandle. Queste sono tutte le informazioni necessarie per caricare i dati e ripristinare lo stato attuale dell'interfaccia utente.

Ripristina stati complessi: riassemblaggio delle parti

Quando l'utente deve tornare all'attività, esistono due scenari possibili per ricreare l'attività:

  • L'attività viene ricreata dopo essere stata interrotta dal sistema. Il sistema ha salvato la query in un bundle di stato dell'istanza salvata e l'interfaccia utente deve passare la query a ViewModel se non viene utilizzato SavedStateHandle. L'ViewModel rileva 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 creata dopo una modifica della configurazione. Poiché l'istanza ViewModel non è stata eliminata, tutte le informazioni dell'elemento ViewModel sono memorizzate nella cache e non è necessario eseguire una nuova query sul database.

Risorse aggiuntive

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

Blog