Modulo SavedState per ViewModel Parte di Android Jetpack.
Come indicato in Salvataggio degli stati della UI, gli oggetti ViewModel possono gestire le modifiche alla configurazione, quindi non devi preoccuparti dello stato nelle rotazioni o in altri casi. Tuttavia, se devi gestire l'interruzione del processo avviata dal sistema, ti consigliamo di utilizzare l'API SavedStateHandle come backup.
Lo stato della UI viene solitamente archiviato o referenziato negli oggetti ViewModel, pertanto l'utilizzo di rememberSaveable in Compose richiede un po' di codice boilerplate che il modulo dello stato salvato può gestire al posto tuo.
Quando utilizzi questo modulo, gli oggetti ViewModel ricevono un oggetto SavedStateHandle tramite il relativo costruttore. Questo oggetto è una mappa chiave-valore che ti consente di scrivere e recuperare oggetti da e verso lo stato salvato. Questi valori persistono dopo che il processo viene terminato dal sistema e rimangono disponibili tramite lo stesso oggetto.
Lo stato salvato è collegato allo stack di attività. Se lo stack di attività scompare, scompare anche lo stato salvato. Ciò può verificarsi quando si forza l'arresto di un'app, si rimuove l'app dal menu Recenti o si riavvia il dispositivo. In questi casi, lo stack di attività scompare e non puoi ripristinare le informazioni nello stato salvato. Negli scenari di chiusura dello stato della UI avviata dall'utente, lo stato salvato non viene ripristinato. Negli scenari avviati dal sistema, sì.
Configurazione
Per utilizzare SavedStateHandle, accettalo come argomento del costruttore per il tuo ViewModel.
class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }
Puoi quindi recuperare un'istanza di ViewModel all'interno dei tuoi componibili senza alcuna configurazione aggiuntiva. La factory ViewModel predefinita fornisce
l'oggetto SavedStateHandle appropriato per il tuo ViewModel.
class MyViewModel : ViewModel() { /*...*/ } // import androidx.lifecycle.viewmodel.compose.viewModel @Composable fun MyScreen( viewModel: MyViewModel = viewModel() ) { // use viewModel here }
Quando fornisci un'istanza ViewModelProvider.Factory personalizzata, puoi
attivare l'utilizzo di SavedStateHandle utilizzando CreationExtras e il
DSL viewModelFactory.
Utilizzo di SavedStateHandle
La classe SavedStateHandle è una mappa chiave-valore che ti consente di scrivere e recuperare dati da e verso lo stato salvato tramite i metodi set() e get().
Utilizzando SavedStateHandle, il valore della query viene conservato anche in caso di interruzione del processo,
garantendo che l'utente veda lo stesso insieme di dati filtrati prima e dopo
la ricreazione senza che l'attività o il frammento debbano salvare, ripristinare
e inoltrare manualmente il valore a ViewModel.
SavedStateHandle ha anche altri metodi che potresti aspettarti quando interagisci
con una mappa chiave-valore:
contains(String key): verifica se esiste un valore per la chiave specificata.remove(String key): rimuove il valore per la chiave specificata.keys(): restituisce tutte le chiavi contenute inSavedStateHandle.
Inoltre, puoi recuperare i valori da SavedStateHandle utilizzando un
holder di dati osservabili. L'elenco dei tipi supportati include:
StateFlow
Puoi recuperare i valori da SavedStateHandle racchiusi in un osservabile StateFlow. A seconda che tu debba modificare direttamente il valore, puoi scegliere tra uno stream di sola lettura o modificabile:
getStateFlow(): utilizza questa opzione se devi solo leggere lo stato. Quando aggiorni il valore della chiave altrove inSavedStateHandle, StateFlow riceve il nuovo valore. Questa opzione è ideale quando vuoi esporre uno stream di sola lettura e trasformarlo utilizzando gli operatori Flow.getMutableStateFlow(): utilizza questo ambito se hai bisogno dell'accesso sia in lettura che in scrittura. L'aggiornamento di.valuedelMutableStateFlowrestituito aggiorna automaticamente ilSavedStateHandlesottostante, evitando di dover impostare manualmente la chiave.
In genere, questi valori vengono aggiornati in base alle interazioni degli utenti, ad esempio l'inserimento di una query per filtrare un elenco di dati.
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { // Use getMutableStateFlow to read and write the query directly private val _query = savedStateHandle.getMutableStateFlow("query", "") val query: StateFlow= _query.asStateFlow() // Use getStateFlow if you only need a read-only stream to react to changes val filteredData: StateFlow<List > = query.flatMapLatest { repository.getFilteredData(it) } .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = emptyList() ) fun setQuery(newQuery: String) { // Updating the MutableStateFlow automatically updates the SavedStateHandle _query.value = newQuery } }
Supporto della serializzazione KotlinX
Per uno stato della UI complesso, puoi utilizzare il delegato della proprietà saved insieme alla serializzazione KotlinX. Questo delegato ti consente di rendere persistenti le classi di dati @Serializable personalizzate direttamente in SavedStateHandle. In questo modo, lo stato di ViewModel viene conservato anche in caso di interruzione del processo, così l'interfaccia utente di Compose può ripristinare facilmente lo stato al momento della ricreazione.
Per utilizzarlo, annota la classe di dati con @Serializable e utilizza il delegato saved nel tuo ViewModel:
import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel // Ensure you have the savedstate-ktx dependency import androidx.savedstate.serialization.saved import kotlinx.serialization.Serializable @Serializable data class UserFilterState( val searchQuery: String, val minAge: Int, val includeInactive: Boolean ) class FilterViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { // The state is automatically serialized to a Bundle on process death, // and deserialized upon recreation. var filterState by savedStateHandle.saved { UserFilterState(searchQuery = "", minAge = 18, includeInactive = false) } fun updateQuery(newQuery: String) { // Mutating the property automatically updates the underlying SavedStateHandle filterState = filterState.copy(searchQuery = newQuery) } }
Supporto di Compose State
Se lo stato si basa sulle API Saver di Compose anziché sulla serializzazione KotlinX, l'artefatto lifecycle-viewmodel-compose fornisce il delegato saveable. In questo modo è possibile l'interoperabilità tra SavedStateHandle e Saver di Compose, in modo che qualsiasi State che puoi salvare tramite rememberSaveable con un Saver personalizzato possa essere salvato anche con SavedStateHandle.
class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() { var filteredData: List<String> by savedStateHandle.saveable { mutableStateOf(emptyList()) } fun setQuery(query: String) { withMutableSnapshot { filteredData += query } } }
Tipi supportati
I dati conservati all'interno di un SavedStateHandle vengono salvati e ripristinati come Bundle,
insieme al resto di savedInstanceState per la tua app.
Tipi supportati direttamente
Per impostazione predefinita, puoi chiamare set() e get() su un SavedStateHandle per gli stessi tipi di dati di un Bundle, come mostrato di seguito:
| Supporto di tipi/classi | Supporto degli array |
double |
double[] |
int |
int[] |
long |
long[] |
String |
String[] |
byte |
byte[] |
char |
char[] |
CharSequence |
CharSequence[] |
float |
float[] |
Parcelable |
Parcelable[] |
Serializable |
Serializable[] |
short |
short[] |
SparseArray |
|
Binder |
|
Bundle |
|
ArrayList |
|
Size (only in API 21+) |
|
SizeF (only in API 21+) |
Se la classe non estende una di quelle nell'elenco precedente, valuta la possibilità di rendere la classe parcelable aggiungendo l'annotazione Kotlin @Parcelize o implementando direttamente Parcelable.
Salvataggio di classi non parcelable
Se una classe non implementa Parcelable o Serializable e non può essere modificata per implementare una di queste interfacce, non è possibile salvare direttamente un'istanza di quella classe in un SavedStateHandle.
A partire da Lifecycle 2.3.0-alpha03, SavedStateHandle ti consente di salvare qualsiasi oggetto fornendo la tua logica per il salvataggio e il ripristino dell'oggetto come Bundle utilizzando il metodo setSavedStateProvider().
SavedStateRegistry.SavedStateProvider è un'interfaccia che definisce un singolo metodo saveState() che restituisce un Bundle contenente lo stato che vuoi salvare. Quando SavedStateHandle è pronto per salvare il proprio stato, chiama
saveState() per recuperare Bundle da SavedStateProvider e salva
Bundle per la chiave associata.
Prendiamo l'esempio di un'app che richiede un'immagine dall'app Fotocamera tramite l'intent ACTION_IMAGE_CAPTURE, passando un file temporaneo in cui la fotocamera deve archiviare l'immagine. TempFileViewModel incapsula la logica per la creazione del file temporaneo.
class TempFileViewModel : ViewModel() { private var tempFile: File? = null fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Per assicurarti che il file temporaneo non vada perso se il processo dell'attività viene interrotto e successivamente ripristinato, TempFileViewModel può utilizzare SavedStateHandle per rendere persistenti i dati. Per consentire a TempFileViewModel di salvare i propri dati, implementa SavedStateProvider e impostalo come provider su SavedStateHandle di ViewModel:
private fun File.saveTempFile() = bundleOf("path", absolutePath) class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
Per ripristinare i dati di File quando l'utente torna, recupera temp_file
Bundle da SavedStateHandle. Si tratta dello stesso Bundle fornito da saveTempFile() che contiene il percorso assoluto. Il percorso assoluto può quindi essere utilizzato per creare un'istanza di un nuovo File.
private fun File.saveTempFile() = bundleOf("path", absolutePath) private fun Bundle.restoreTempFile() = if (containsKey("path")) { File(getString("path")) } else { null } class TempFileViewModel(savedStateHandle: SavedStateHandle) : ViewModel() { private var tempFile: File? = null init { val tempFileBundle = savedStateHandle.get<Bundle>("temp_file") if (tempFileBundle != null) { tempFile = tempFileBundle.restoreTempFile() } savedStateHandle.setSavedStateProvider("temp_file") { // saveState() if (tempFile != null) { tempFile.saveTempFile() } else { Bundle() } } } fun createOrGetTempFile(): File { return tempFile ?: File.createTempFile("temp", null).also { tempFile = it } } }
SavedStateHandle nei test
Per testare un ViewModel che accetta un SavedStateHandle come dipendenza, crea una nuova istanza di SavedStateHandle con i valori di test richiesti e passala all'istanza ViewModel che stai testando.
class MyViewModelTest { private lateinit var viewModel: MyViewModel @Before fun setup() { val savedState = SavedStateHandle(mapOf("someIdArg" to testId)) viewModel = MyViewModel(savedState = savedState) } }
Risorse aggiuntive
Per ulteriori informazioni sul modulo Saved State per ViewModel, consulta le seguenti risorse.
Codelab
Visualizza contenuti
Consigliati per te
- Nota: il testo del link viene visualizzato quando JavaScript è disattivato
- Salva gli stati della UI
- Utilizza gli oggetti dati osservabili
- Crea ViewModel con dipendenze