Salva stati UI

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 dopo che il sistema distrugge l'attività host o il processo dell'applicazione è essenziale per una buona esperienza utente. Gli utenti si aspettano che lo stato dell'interfaccia utente rimanga invariato, ma il sistema potrebbe eliminare l'Activity che ospita la schermata 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:

La soluzione ottimale dipende dalla complessità dei dati dell'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 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, un utente si aspetta che lo stato dell'interfaccia utente 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 passa a una schermata, lo stato dell'interfaccia utente temporanea rimanga invariato finché non lo chiude completamente. L'utente può chiudere completamente una schermata o un'app nel seguente modo:

  • Scorrendo l'app 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 la schermata e che, se torna, si aspetti che la schermata si avvii da uno stato pulito. Il comportamento del sistema sottostante per questi scenari di chiusura corrisponde alle aspettative dell'utente: l'istanza dell'attività host verrà eliminata e rimossa dalla memoria, insieme a qualsiasi stato memorizzato e a qualsiasi record di stato salvato associato.

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 una schermata 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à host quando si verifica una modifica alla configurazione, eliminando qualsiasi stato dell'UI memorizzato. Per scoprire di più sulle configurazioni dei dispositivi, consulta Reagire alle modifiche alla configurazione in Jetpack Compose.

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 app 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 su una schermata e poi preme il pulsante Home o risponde a una chiamata. Quando torna alla schermata 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 messa 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'attività host viene eliminata, insieme a qualsiasi stato memorizzato. Quando l'utente riavvia l'app, la schermata è inaspettatamente 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 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 completa dello schermo da parte 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 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 proprietario del ciclo di vita, ad esempio una destinazione di navigazione o un'attività. Rimane in memoria durante una modifica della configurazione e il sistema associa automaticamente il ViewModel alla nuova istanza del proprietario del ciclo di vita risultante dalla modifica della configurazione.

A differenza dello stato salvato, i ViewModel vengono eliminati durante un'interruzione del processo avviata dal sistema. Per ricaricare i dati dopo l'interruzione di un processo avviato dal sistema in un ViewModel, utilizza l'API SavedStateHandle. In alternativa, se i dati sono correlati alla UI e non devono essere conservati in ViewModel, utilizza rememberSerializable. Per tipi di dati primitivi o scenari in cui non vuoi utilizzare @Serializable, utilizza rememberSaveable. 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 salvato come backup per gestire l'interruzione del processo avviata dal sistema

API come rememberSerializable e rememberSaveable in Compose e SavedStateHandle in ViewModels archiviano i dati necessari per ricaricare lo stato dell'UI se il sistema elimina e ricrea un componente in un secondo momento. Per gestire in modo più efficiente strutture di dati complesse, SavedStateHandle supporta la serializzazione Kotlinx tramite l'estensione saved {}, che consente di rendere persistenti e ripristinare senza problemi oggetti type-safe insieme ai tipi primitivi standard. Per scoprire come implementare lo stato salvato utilizzando rememberSaveable, consulta Stato e Jetpack Compose.

I bundle di stato 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 salvato per archiviare grandi quantità di dati, ad esempio 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 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 aver bisogno di utilizzare lo stato salvato. 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 salvato e persistere 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à. Se un insieme di dati sullo stato dell'interfaccia utente, ad esempio una query di ricerca, è stato trasmesso come extra dell'intent all'avvio dell'attività, puoi utilizzare il bundle di extra anziché il bundle dello stato salvato. Per saperne di più sugli extra intent, consulta Intent e filtri per intent.

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 le API per lo stato salvato da sole 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 componenti UI, come ComponentActivity, implementano SavedStateRegistryOwner e forniscono un SavedStateRegistry associato a quel componente. SavedStateRegistry consente ai componenti di agganciarsi allo stato salvato 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 proprietario del ciclo di vita chiamando savedStateRegistry.

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 del ciclo di vita del proprietario del ciclo di vita.

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

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 ComponentActivity, 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.

  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 SearchActivity : ComponentActivity() {
    private var searchManager = SearchManager(this)

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // Set up your Compose UI here
        setContent {
            // ...
        }
    }
  }

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

L'archiviazione locale permanente, ad esempio un database o DataStore, verrà mantenuta 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'applicazione avviata dal sistema, 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'app.

Né ViewModel né lo stato salvato utilizzando rememberSerializable, rememberSaveable o SavedStateHandle 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'app, in base ai compromessi tra complessità, velocità di accesso e durata dei dati:

  • Persistenza locale: archivia tutti i dati dell'applicazione che non vuoi perdere se apri e chiudi l'app.
    • 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 salvato (rememberSerializable, rememberSaveable e SavedStateHandle): memorizza una piccola quantità di dati necessari per ricaricare lo stato della UI se il sistema si arresta e poi ricrea la UI. Anziché archiviare oggetti complessi qui, mantieni gli oggetti complessi nell'archivio locale e memorizza un ID univoco per questi oggetti nelle API di stato salvato.
    • Esempio: memorizzazione della query di ricerca più recente.

Ad esempio, considera un'app 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 dello schermo.

Quando l'app passa in background e il sistema salva lo stato, la query di ricerca deve essere archiviata utilizzando le API di stato salvato, nel caso in cui il processo venga ricreato. Poiché le informazioni sono necessarie per caricare i dati dell'applicazione archiviati in questo, memorizza la query di ricerca in ViewModel SavedStateHandle o utilizza rememberSerializable o rememberSaveable nei composables. 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 che l'utente torni all'app, esistono due possibili scenari per ricreare la UI:

  • L'interfaccia utente viene ricreata dopo che il sistema termina la procedura di applicazione. Il sistema ha salvato la query utilizzando le API di stato salvato. ViewModel (utilizzando SavedStateHandle) o il componente componibile (utilizzando rememberSerializable o rememberSaveable) ripristina automaticamente la query. Se il composable ripristina la query, la passa a ViewModel. 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'interfaccia utente viene ricreata dopo una modifica della 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.

Codelab

Visualizza contenuti