In Jetpack Compose, le funzioni componibili spesso mantengono lo stato utilizzando la funzione remember. I valori memorizzati possono essere riutilizzati in più ricomposizioni, come
spiegato in Stato e Jetpack Compose.
Anche se remember funge da strumento per rendere persistenti i valori tra le ricomposizioni, lo stato
spesso deve esistere oltre la durata di una composizione. Questa pagina spiega la
differenza tra le API remember, retain, rememberSaveable,
e rememberSerializable, quando scegliere quale API e quali sono le
best practice per la gestione dei valori memorizzati e conservati in Compose.
Scegliere la durata corretta
In Compose, esistono diverse funzioni che puoi utilizzare per mantenere lo stato tra le composizioni e non solo: remember, retain, rememberSaveable e rememberSerializable. Queste funzioni differiscono per durata e semantica
e sono adatte per memorizzare tipi specifici di stato. Le differenze sono
descritte nella tabella seguente:
|
|
|
|
|---|---|---|---|
I valori sopravvivono alle ricomposizioni? |
✅ |
✅ |
✅ |
I valori sopravvivono alle ricreazioni dell'attività? |
❌ |
✅ Verrà sempre restituita la stessa istanza ( |
✅ Verrà restituito un oggetto equivalente ( |
I valori sopravvivono all'interruzione del processo? |
❌ |
❌ |
✅ |
Tipi di dati supportati |
Tutte |
Non deve fare riferimento a oggetti che verrebbero divulgati se l'attività viene eliminata |
Deve essere serializzabile |
Casi d'uso |
|
|
|
remember
remember è il modo più comune per memorizzare lo stato in Compose. Quando remember viene
chiamato per la prima volta, il calcolo specificato viene eseguito e
memorizzato, ovvero viene archiviato da Compose per essere riutilizzato in futuro dal
composable. Quando un elemento componibile viene ricomposto, il suo codice viene eseguito di nuovo, ma tutte le
chiamate a remember restituiscono i valori della composizione precedente anziché
eseguire nuovamente il calcolo.
Ogni istanza di una funzione componibile ha il proprio insieme di valori memorizzati, denominato memorizzazione posizionale. Quando i valori memorizzati vengono memorizzati nella cache per l'utilizzo in più ricomposizioni, sono associati alla loro posizione nella gerarchia di composizione. Se un componente componibile viene utilizzato in posizioni diverse, ogni istanza nella gerarchia di composizione ha il proprio insieme di valori memorizzati.
Quando un valore memorizzato non viene più utilizzato, viene dimenticato e il relativo record viene eliminato. I valori memorizzati vengono dimenticati quando vengono rimossi dalla
gerarchia di composizione (incluso quando un valore viene rimosso e aggiunto di nuovo per spostarlo
in una posizione diversa senza l'utilizzo di key o
MovableContent) o chiamati con parametri key diversi.
Tra le scelte disponibili, remember ha la durata più breve e dimentica
i valori prima delle quattro funzioni di memorizzazione descritte in questa pagina.
Per questo motivo, è più adatta a:
- Creazione di oggetti di stato interni, ad esempio la posizione di scorrimento o lo stato dell'animazione
- Evitare la ricreazione costosa degli oggetti a ogni ricomposizione
Tuttavia, dovresti evitare:
- Memorizzare qualsiasi input dell'utente con
remember, perché gli oggetti memorizzati vengono dimenticati in seguito alle modifiche alla configurazione dell'attività e all'interruzione del processo avviata dal sistema.
rememberSaveable e rememberSerializable
rememberSaveable e rememberSerializable si basano su remember. Hanno la durata più lunga tra le funzioni di memoizzazione descritte in questa guida.
Oltre a memorizzare gli oggetti in base alla posizione nelle ricomposizioni, può anche
salvare i valori in modo che possano essere ripristinati nelle ricreazioni delle attività,
incluse quelle dovute a modifiche alla configurazione e all'interruzione del processo (quando il sistema interrompe
il processo dell'app in background, di solito per liberare memoria
per le app in primo piano o se l'utente revoca le autorizzazioni dell'app mentre è
in esecuzione).
rememberSerializable funziona allo stesso modo di rememberSaveable, ma
supporta automaticamente la persistenza di tipi complessi serializzabili con la
libreria kotlinx.serialization. Scegli rememberSerializable se il tuo tipo è
(o può essere) contrassegnato con @Serializable e rememberSaveable in tutti gli altri
casi.
In questo modo, sia rememberSaveable che rememberSerializable sono candidati perfetti
per memorizzare lo stato associato all'input dell'utente, inclusi l'inserimento nel campo di testo, la posizione
di scorrimento, gli stati di attivazione/disattivazione e così via. Devi salvare questo stato per assicurarti che l'utente
non perda mai la sua posizione. In generale, devi utilizzare rememberSaveable o
rememberSerializable per memorizzare qualsiasi stato che la tua app non è in grado di recuperare
da un'altra origine dati persistente, ad esempio un database.
Tieni presente che rememberSaveable e rememberSerializable salvano i valori memorizzati
serializzandoli in un Bundle. Ciò ha due conseguenze:
- I valori che memorizzi devono essere rappresentabili da uno o più dei seguenti tipi di dati: primitivi (inclusi
Int,Long,Float,Double),Stringo array di uno qualsiasi di questi tipi. - Quando viene ripristinato un valore salvato, si tratta di una nuova istanza uguale a
(
==), ma non dello stesso riferimento (===) utilizzato in precedenza dalla composizione.
Per archiviare tipi di dati più complessi senza utilizzare kotlinx.serialization, puoi implementare un Saver personalizzato per serializzare e deserializzare l'oggetto nei tipi di dati supportati. Tieni presente che Compose comprende tipi di dati comuni come
State, List, Map, Set e così via, e li converte automaticamente
in tipi supportati per tuo conto. Di seguito è riportato un esempio di
Saver per una classe Size. Viene implementato inserendo tutte le proprietà di Size in un elenco utilizzando listSaver.
data class Size(val x: Int, val y: Int) { object Saver : androidx.compose.runtime.saveable.Saver<Size, Any> by listSaver( save = { listOf(it.x, it.y) }, restore = { Size(it[0], it[1]) } ) } @Composable fun rememberSize(x: Int, y: Int) { rememberSaveable(x, y, saver = Size.Saver) { Size(x, y) } }
retain
L'API retain si trova tra remember e
rememberSaveable/rememberSerializable in termini di durata della memorizzazione
dei valori. Il nome è diverso perché i valori conservati hanno un ciclo di vita diverso rispetto a quelli memorizzati.
Quando un valore viene conservato, viene memorizzato nella cache in base alla posizione e salvato in una
struttura di dati secondaria con una durata separata, legata alla durata dell'app. Un valore conservato è in grado di sopravvivere alle modifiche alla configurazione senza
essere serializzato, ma non può sopravvivere alla chiusura del processo. Se un valore non viene utilizzato dopo la ricreazione della gerarchia di composizione, il valore conservato viene ritirato (ovvero l'equivalente di essere dimenticato in retain).
In cambio di questo ciclo di vita più breve di rememberSaveable, retain è in grado
di rendere persistenti i valori che non possono essere serializzati, come espressioni lambda, flussi e
oggetti di grandi dimensioni come bitmap. Ad esempio, puoi utilizzare retain per gestire un lettore multimediale (come ExoPlayer) per evitare interruzioni della riproduzione multimediale durante una modifica della configurazione.
@Composable fun MediaPlayer() { // Use the application context to avoid a memory leak val applicationContext = LocalContext.current.applicationContext val exoPlayer = retain { ExoPlayer.Builder(applicationContext).apply { /* ... */ }.build() } // ... }
retain contro ViewModel
Entrambi retain e ViewModel offrono funzionalità simili nella loro capacità più comunemente utilizzata di rendere persistenti le istanze degli oggetti tra le modifiche alla configurazione. La scelta di utilizzare retain o ViewModel dipende
dal tipo di valore che vuoi rendere persistente, dal modo in cui deve essere definito l'ambito e se
hai bisogno di funzionalità aggiuntive.
I ViewModel sono oggetti che in genere incapsulano la comunicazione tra
i livelli UI e dati della tua app. Consentono di spostare la logica dalle funzioni componibili, il che migliora la testabilità. Gli ViewModel vengono gestiti come singleton all'interno di un ViewModelStore e hanno una durata diversa rispetto ai valori conservati. Un ViewModel rimarrà attivo finché il relativo ViewModelStore non verrà eliminato, mentre i valori conservati vengono ritirati quando i contenuti vengono rimossi definitivamente dalla composizione (ad esempio, per una modifica alla configurazione, un valore conservato viene ritirato se la gerarchia della UI viene ricreata e il valore conservato non è stato utilizzato dopo la ricreazione della composizione).
ViewModel include anche integrazioni predefinite per l'inserimento delle dipendenze
con Dagger e Hilt, l'integrazione con SavedState e il supporto integrato delle coroutine
per l'avvio di attività in background. Ciò rende ViewModel un luogo ideale per
avviare attività in background e richieste di rete, interagire con altre origini dati
nel tuo progetto e, facoltativamente, acquisire e rendere persistente lo stato dell'interfaccia utente mission-critical
che deve essere mantenuto in caso di modifiche alla configurazione in ViewModel e
sopravvivere all'interruzione del processo.
retain è più adatto a oggetti con ambito limitato a istanze di componenti componibili specifiche e che non richiedono il riutilizzo o la condivisione tra componenti componibili di pari livello. Mentre
ViewModel è un buon posto per archiviare lo stato della UI ed eseguire attività in background,
retain è un buon candidato per archiviare oggetti per l'infrastruttura della UI come cache,
monitoraggio delle impressioni e analisi, dipendenze da AndroidView e altri
oggetti che interagiscono con il sistema operativo Android o gestiscono librerie di terze parti come
processori di pagamento o pubblicità.
Per gli utenti avanzati che progettano pattern di architettura delle app personalizzati al di fuori dei
suggerimenti per l'architettura delle app per Android moderne: retain può essere utilizzato anche per
creare un'API interna "simile a ViewModel". Sebbene il supporto per le coroutine e
lo stato salvato non sia offerto immediatamente, retain può fungere da elemento
di base per il ciclo di vita di ViewModel simili con queste funzionalità
integrate. I dettagli su come progettare un componente di questo tipo non rientrano nell'ambito di questa guida.
|
|
|
|---|---|---|
Scoping |
Nessun valore condiviso; ogni valore viene conservato e associato a un punto specifico della gerarchia di composizione. Il mantenimento dello stesso tipo in una posizione diversa agisce sempre su una nuova istanza. |
|
Distruzione |
Quando esci definitivamente dalla gerarchia di composizione |
Quando |
Funzionalità aggiuntive |
Può ricevere callback quando l'oggetto si trova nella gerarchia di composizione o meno |
|
Di proprietà di |
|
|
Casi d'uso |
|
|
Combina retain e rememberSaveable o rememberSerializable
A volte, un oggetto deve avere una durata ibrida sia di retained sia di
rememberSaveable o rememberSerializable. Questo potrebbe indicare che il tuo
oggetto dovrebbe essere un ViewModel, che può supportare lo stato salvato come descritto nella
guida al modulo Saved State per ViewModel.
è possibile utilizzare retain e rememberSaveable o rememberSerializable
contemporaneamente. La combinazione corretta di entrambi i cicli di vita aggiunge una complessità significativa.
Ti consigliamo di utilizzare questo pattern nell'ambito di pattern di architettura più avanzati e personalizzati e solo quando si verificano tutte le seguenti condizioni:
- Stai definendo un oggetto composto da un mix di valori che devono essere conservati o salvati (ad es. un oggetto che tiene traccia di un input utente e una cache in memoria che non può essere scritta su disco)
- Il tuo stato è limitato a un composable e non è adatto all'ambito singleton
o alla durata di
ViewModel
Quando si verificano tutte queste condizioni, ti consigliamo di dividere la classe in tre parti: i dati salvati, i dati conservati e un oggetto "mediatore" che non ha uno stato proprio e delega agli oggetti conservati e salvati l'aggiornamento dello stato di conseguenza. Questo pattern assume la seguente forma:
@Composable fun rememberAndRetain(): CombinedRememberRetained { val saveData = rememberSerializable(serializer = serializer<ExtractedSaveData>()) { ExtractedSaveData() } val retainData = retain { ExtractedRetainData() } return remember(saveData, retainData) { CombinedRememberRetained(saveData, retainData) } } @Serializable data class ExtractedSaveData( // All values that should persist process death should be managed by this class. var savedData: AnotherSerializableType = defaultValue() ) class ExtractedRetainData { // All values that should be retained should appear in this class. // It's possible to manage a CoroutineScope using RetainObserver. // See the full sample for details. var retainedData = Any() } class CombinedRememberRetained( private val saveData: ExtractedSaveData, private val retainData: ExtractedRetainData, ) { fun doAction() { // Manipulate the retained and saved state as needed. } }
Separando lo stato in base alla durata, la separazione delle responsabilità e
dell'archiviazione diventa molto esplicita. È intenzionale che i dati di salvataggio non possano essere
manipolati dai dati di conservazione, in quanto ciò impedisce uno scenario in cui viene tentato un aggiornamento dei dati di salvataggio
quando il bundle savedInstanceState è già stato acquisito e
non può essere aggiornato. Consente inoltre di testare scenari di ricreazione testando i costruttori senza chiamare Compose o simulare la ricreazione di un'attività.
Vedi l'esempio completo (RetainAndSaveSample.kt) per un esempio completo di
come può essere implementato questo pattern.
Memorizzazione posizionale e layout adattivi
Le applicazioni Android possono supportare molti fattori di forma, tra cui smartphone, pieghevoli, tablet e computer. Le applicazioni devono spesso passare da un fattore di forma all'altro utilizzando layout adattivi. Ad esempio, un'app in esecuzione su un tablet potrebbe essere in grado di mostrare una visualizzazione elenco-dettagli a due colonne, ma potrebbe spostarsi tra un elenco e una pagina di dettagli se visualizzata sullo schermo più piccolo di uno smartphone.
Poiché i valori memorizzati e conservati vengono memorizzati nella cache in base alla posizione, vengono riutilizzati solo se vengono visualizzati nello stesso punto della gerarchia di composizione. Man mano che i layout si adattano a fattori di forma diversi, possono alterare la struttura della gerarchia di composizione e portare a valori dimenticati.
Per i componenti predefiniti come ListDetailPaneScaffold e NavDisplay
(da Jetpack Navigation 3), questo non è un problema e lo stato verrà mantenuto
durante le modifiche al layout. Per i componenti personalizzati che si adattano ai fattori di forma,
assicurati che lo stato non sia interessato dalle modifiche al layout eseguendo una delle seguenti operazioni:
- Assicurati che i composable stateful vengano sempre chiamati nello stesso punto della gerarchia di composizione. Implementa layout adattivi modificando la logica del layout anziché riposizionare gli oggetti nella gerarchia di composizione.
- Utilizza
MovableContentper riposizionare i componibili stateful in modo controllato. Le istanze diMovableContentsono in grado di spostare i valori memorizzati e conservati dalle vecchie alle nuove posizioni.
Ricorda le funzioni di fabbrica
Sebbene le UI di Compose siano costituite da funzioni componibili, molti oggetti vengono utilizzati per
la creazione e l'organizzazione di una composizione. L'esempio più comune è quello di oggetti componibili complessi che definiscono il proprio stato, come LazyList, che accetta un LazyListState.
Quando definisci oggetti incentrati su Compose, ti consigliamo di creare una funzione remember
per definire il comportamento di memorizzazione previsto, inclusi sia la durata
che gli input chiave. In questo modo, i consumatori del tuo stato possono creare con sicurezza istanze nella gerarchia di composizione che sopravviveranno e verranno invalidate come previsto. Quando definisci una funzione di fabbrica componibile, segui queste linee guida:
- Aggiungi il prefisso
rememberal nome della funzione. Se l'implementazione della funzione dipende dall'oggettoretainede l'API non si evolverà mai per fare affidamento su una variante diversa diremember, utilizza il prefissoretain. - Utilizza
rememberSaveableorememberSerializablese è stata scelta la persistenza dello stato ed è possibile scrivere un'implementazioneSavercorretta. - Evita effetti collaterali o valori di inizializzazione basati su
CompositionLocalche potrebbero non essere pertinenti all'utilizzo. Ricorda che il luogo in cui viene creato lo stato potrebbe non essere quello in cui viene utilizzato.
@Composable fun rememberImageState( imageUri: String, initialZoom: Float = 1f, initialPanX: Int = 0, initialPanY: Int = 0 ): ImageState { return rememberSaveable(imageUri, saver = ImageState.Saver) { ImageState( imageUri, initialZoom, initialPanX, initialPanY ) } } data class ImageState( val imageUri: String, val zoom: Float, val panX: Int, val panY: Int ) { object Saver : androidx.compose.runtime.saveable.Saver<ImageState, Any> by listSaver( save = { listOf(it.imageUri, it.zoom, it.panX, it.panY) }, restore = { ImageState(it[0] as String, it[1] as Float, it[2] as Int, it[3] as Int) } ) }