Module Saved State pour ViewModel   Inclus dans Android Jetpack.

Comme indiqué dans la section Enregistrer des états d'interface utilisateur, les objets ViewModel peuvent gérer les modifications de configuration. Vous n'avez donc pas à vous soucier de l'état dans les cas de rotation ou autres. Toutefois, si vous devez gérer l'arrêt du processus initié par le système, vous pouvez utiliser l'API SavedStateHandle comme sauvegarde.

L'état de l'interface utilisateur est généralement stocké ou référencé dans des objets ViewModel. Par conséquent, l'utilisation de rememberSaveable dans Compose nécessite un code récurrent que le module d'état enregistré peut gérer pour vous.

Lorsque vous utilisez ce module, les ViewModel objets reçoivent un SavedStateHandle objet via son constructeur. Cet objet est un mappage clé-valeur qui vous permet d'écrire et de récupérer des objets depuis et vers l'état enregistré. Ces valeurs sont conservées après la fin du processus par le système et restent disponibles via le même objet.

L'état enregistré est lié à votre pile de tâches. Si celle-ci disparaît, l'état enregistré aussi. Cela peut se produire lors de l'arrêt forcé d'une application, de sa suppression du menu "Applications récentes" ou du redémarrage de l'appareil. Dans ce cas, la pile de tâches disparaît et vous ne pouvez pas restaurer les informations de l'état enregistré. Dans les scénarios d'arrêt de l'état de l'interface utilisateur déclenché par l'utilisateur , l'état enregistré n'est pas restauré. Dans les scénarios initiés par le système, il l'est.

Configuration

Pour utiliser SavedStateHandle, acceptez-le comme argument de constructeur dans votre ViewModel.

class SavedStateViewModel(private val state: SavedStateHandle) : ViewModel() { ... }

Vous pouvez ensuite récupérer une instance de votre ViewModel dans vos composables sans configuration supplémentaire. La fabrique ViewModel par défaut fournit le SavedStateHandle approprié à votre ViewModel.

class MyViewModel : ViewModel() { /*...*/ }

// import androidx.lifecycle.viewmodel.compose.viewModel
@Composable
fun MyScreen(
    viewModel: MyViewModel = viewModel()
) {
    // use viewModel here
}

Lorsque vous fournissez une instance ViewModelProvider.Factory personnalisée, vous pouvez activer l'utilisation de SavedStateHandle à l'aide de CreationExtras et du viewModelFactory DSL.

Utiliser SavedStateHandle

La classe SavedStateHandle est un mappage clé-valeur qui vous permet d'écrire et de récupérer des données depuis et vers l'état enregistré via les méthodes set() et get().

En utilisant SavedStateHandle, la valeur de la requête est conservée après l'arrêt du processus. Ainsi, l'utilisateur voit le même ensemble de données filtrées avant et après la recréation, sans que l'activité ou le fragment n'aient besoin d'enregistrer, de restaurer ni de transmettre manuellement cette valeur au ViewModel.

SavedStateHandle propose également d'autres méthodes auxquelles vous vous attendez peut-être pour interagir avec un mappage clé/valeur :

Vous pouvez également récupérer les valeurs de SavedStateHandle à l'aide d'un conteneur de données observable. Voici la liste des types acceptés :

StateFlow

Vous pouvez récupérer les valeurs de SavedStateHandle encapsulées dans un observable StateFlow. Selon que vous devez modifier directement la valeur ou non, vous pouvez choisir entre un flux en lecture seule ou mutable :

  • getStateFlow() : utilisez cette option si vous n'avez besoin que de lire l'état. Lorsque vous mettez à jour la valeur de la clé ailleurs dans le SavedStateHandle, le StateFlow reçoit la nouvelle valeur. Cette option est idéale lorsque vous souhaitez exposer un flux en lecture seule et le transformer à l'aide d'opérateurs Flow.
  • getMutableStateFlow() : utilisez cette option si vous avez besoin d'un accès en lecture et en écriture. La mise à jour de la .value du MutableStateFlow renvoyé met automatiquement à jour le SavedStateHandle sous-jacent, ce qui vous évite d'avoir à définir manuellement la clé.

Le plus souvent, vous mettez à jour ces valeurs en raison des interactions des utilisateurs, comme la saisie d'une requête pour filtrer une liste de données.

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

Prise en charge de la sérialisation KotlinX

Pour un état d'interface utilisateur complexe, vous pouvez utiliser le délégué de propriété saved avec la sérialisation KotlinX. Ce délégué vous permet de conserver les classes de données @Serializable personnalisées directement dans le SavedStateHandle. Cela préserve l'état de votre ViewModel jusqu'à l'arrêt du processus, de sorte que votre interface utilisateur Compose peut restaurer son état de manière transparente lors de la recréation.

Pour l'utiliser, annotez votre classe de données avec @Serializable et utilisez le délégué saved dans votre 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)
    }
}

Prise en charge de l'état de Compose

Si votre état repose sur les API Saver de Compose plutôt que sur la sérialisation KotlinX, l'artefact lifecycle-viewmodel-compose fournit le délégué saveable. Cela permet l'interopérabilité entre SavedStateHandle et le Saver de Compose afin que tout State que vous pouvez enregistrer via rememberSaveable avec un Saver personnalisé puisse également être enregistré avec SavedStateHandle.

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {

    var filteredData: List<String> by savedStateHandle.saveable {
        mutableStateOf(emptyList())
    }

    fun setQuery(query: String) {
        withMutableSnapshot {
            filteredData += query
        }
    }
}

Types pris en charge

Les données conservées dans un SavedStateHandle sont enregistrées et restaurées en tant que Bundle, avec le reste de savedInstanceState pour votre application.

Types directement pris en charge

Par défaut, vous pouvez appeler set() et get() sur un SavedStateHandle pour les mêmes types de données qu'un Bundle, comme indiqué ci-dessous :

Prise en charge du type/de la classe Prise en charge des tableaux
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+)

Si la classe n'étend pas l'un des éléments de la liste ci-dessus, vous pouvez rendre la classe parcelable en ajoutant l'annotation Kotlin @Parcelize ou en implémentant Parcelable directement.

Enregistrer des classes non parcelables

Si une classe n'implémente pas Parcelable ou Serializable et ne peut pas être modifiée pour implémenter l'une de ces interfaces, il n'est pas possible d'enregistrer directement une instance de cette classe dans un SavedStateHandle.

À partir du cycle de vie 2.3.0-alpha03, SavedStateHandle vous permet d'enregistrer un objet en fournissant votre propre logique d'enregistrement et de restauration en tant qu' objet Bundle à l'aide de la méthode setSavedStateProvider(). SavedStateRegistry.SavedStateProvider est une interface qui définit une seule méthode saveState() qui renvoie un Bundle contenant l'état que vous souhaitez enregistrer. Lorsque SavedStateHandle est prêt à enregistrer son état, il appelle saveState() pour récupérer le Bundle à partir du SavedStateProvider et enregistre le Bundle pour la clé associée.

Prenons l'exemple d'une application qui demande une image à l'appli Appareil photo via l'intent ACTION_IMAGE_CAPTURE, en transmettant un fichier temporaire dans lequel l'appareil photo doit stocker l'image. TempFileViewModel encapsule la logique de création de ce fichier temporaire.

class TempFileViewModel : ViewModel() {
    private var tempFile: File? = null

    fun createOrGetTempFile(): File {
        return tempFile ?: File.createTempFile("temp", null).also {
            tempFile = it
        }
    }
}

Pour éviter de perdre le fichier temporaire si le processus de l'activité est arrêté, puis restauré, TempFileViewModel peut utiliser SavedStateHandle pour conserver ses données. Pour autoriser TempFileViewModel à enregistrer ses données, implémentez SavedStateProvider et définissez-le comme fournisseur sur le SavedStateHandle de le 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
        }
    }
}

Pour restaurer les données File lorsque l'utilisateur revient, récupérez le temp_file Bundle à partir du SavedStateHandle. Il s'agit de la même valeur Bundle fournie par saveTempFile() qui contient le chemin absolu. Le chemin absolu peut ensuite être utilisé pour instancier un nouveau 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
      }
    }
}

SavedStateHandler dans les tests

Pour tester un ViewModel qui utilise SavedStateHandle comme dépendance, créez une instance de SavedStateHandle avec les valeurs de test requises et transmettez-la à l'instance ViewModel que vous testez.

class MyViewModelTest {

    private lateinit var viewModel: MyViewModel

    @Before
    fun setup() {
        val savedState = SavedStateHandle(mapOf("someIdArg" to testId))
        viewModel = MyViewModel(savedState = savedState)
    }
}

Ressources supplémentaires

Pour en savoir plus sur le module Saved State pour ViewModel, consultez les ressources suivantes.

Ateliers de programmation

Afficher le contenu