Module Saved State pour ViewModel Fait partie d'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 SavedStateHandle comme sauvegarde.

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

Lorsque vous utilisez ce module, les objets ViewModel reçoivent un objet SavedStateHandle 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é. C'est le cas pour les scénarios initiés par le système.

Configuration

À partir de la version Fragment 1.2.0 ou de sa dépendance transitive Activity 1.1.0, vous pouvez accepter un objet SavedStateHandle comme argument de constructeur de votre ViewModel.

Kotlin

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

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle state;

    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        state = savedStateHandle;
    }

    ...
}

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

Kotlin

class MainFragment : Fragment() {
    val vm: SavedStateViewModel by viewModels()

    ...
}

Java

class MainFragment extends Fragment {
    private SavedStateViewModel vm;

    public void onViewCreated(@NonNull View view, Bundle savedInstanceState) {
        vm = new ViewModelProvider(this).get(SavedStateViewModel.class);

        ...


    }

    ...
}

Lorsque vous fournissez une instance ViewModelProvider.Factory personnalisée, vous pouvez activer l'utilisation de SavedStateHandle en étendant AbstractSavedStateViewModelFactory.

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 jusqu'à la fin 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 :

  • contains(String key) : vérifie s'il existe une valeur pour la clé donnée.
  • remove(String key) : supprime la valeur de la clé donnée.
  • keys() : renvoie toutes les clés contenues dans l'élément SavedStateHandle.

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 :

LiveData

Récupérez les valeurs SavedStateHandle qui sont encapsulées dans un objet LiveData observable à l'aide de getLiveData(). Lorsque la valeur de la clé est mise à jour, LiveData reçoit la nouvelle valeur. Le plus souvent, cette valeur est définie en raison des interactions des utilisateurs, comme la saisie d'une requête pour filtrer une liste de données. Cette valeur mise à jour peut ensuite être utilisée pour transformer LiveData.

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: LiveData<List<String>> =
        savedStateHandle.getLiveData<String>("query").switchMap { query ->
        repository.getFilteredData(query)
    }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

Java

public class SavedStateViewModel extends ViewModel {
    private SavedStateHandle savedStateHandle;
    public LiveData<List<String>> filteredData;
    public SavedStateViewModel(SavedStateHandle savedStateHandle) {
        this.savedStateHandle = savedStateHandle;
        LiveData<String> queryLiveData = savedStateHandle.getLiveData("query");
        filteredData = Transformations.switchMap(queryLiveData, query -> {
            return repository.getFilteredData(query);
        });
    }

    public void setQuery(String query) {
        savedStateHandle.set("query", query);
    }
}

StateFlow

Récupérez les valeurs SavedStateHandle qui sont encapsulées dans un objet StateFlow observable à l'aide de getStateFlow(). Lorsque vous mettez à jour la valeur de la clé, StateFlow reçoit la nouvelle valeur. La plupart du temps, vous pouvez définir la valeur en fonction des interactions de l'utilisateur, comme la saisie d'une requête pour filtrer une liste de données. Vous pouvez ensuite transformer cette valeur mise à jour à l'aide d'autres opérateurs de flux.

Kotlin

class SavedStateViewModel(private val savedStateHandle: SavedStateHandle) : ViewModel() {
    val filteredData: StateFlow<List<String>> =
        savedStateHandle.getStateFlow<String>("query")
            .flatMapLatest { query ->
                repository.getFilteredData(query)
            }

    fun setQuery(query: String) {
        savedStateHandle["query"] = query
    }
}

Prise en charge expérimentale de State par Compose

L'artefact lifecycle-viewmodel-compose fournit les API expérimentales saveable qui permettent l'interopérabilité entre SavedStateHandle et Saver de Compose. Ainsi, tout State que vous pouvez enregistrer via rememberSaveable avec un Saver personnalisé peut également l'être avec SavedStateHandle.

Kotlin

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 l'activité ou le fragment.

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 la rendre parcelable en ajoutant l'annotation Kotlin @Parcelize ou en implémentant directement Parcelable.

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.

Kotlin

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

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

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel() {
    }


    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }
}

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 du ViewModel :

Kotlin

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

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        savedStateHandle.setSavedStateProvider("temp_file",
            new TempFileSavedStateProvider());
    }
    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }
    }
}

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.

Kotlin

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

Java

class TempFileViewModel extends ViewModel {
    private File tempFile = null;

    public TempFileViewModel(SavedStateHandle savedStateHandle) {
        Bundle tempFileBundle = savedStateHandle.get("temp_file");
        if (tempFileBundle != null) {
            tempFile = TempFileSavedStateProvider.restoreTempFile(tempFileBundle);
        }
        savedStateHandle.setSavedStateProvider("temp_file", new TempFileSavedStateProvider());
    }

    @NonNull
    public File createOrGetTempFile() {
        if (tempFile == null) {
            tempFile = File.createTempFile("temp", null);
        }
        return tempFile;
    }

    private class TempFileSavedStateProvider implements SavedStateRegistry.SavedStateProvider {
        @NonNull
        @Override
        public Bundle saveState() {
            Bundle bundle = new Bundle();
            if (tempFile != null) {
                bundle.putString("path", tempFile.getAbsolutePath());
            }
            return bundle;
        }

        @Nullable
        private static File restoreTempFile(Bundle bundle) {
            if (bundle.containsKey("path") {
                return File(bundle.getString("path"));
            }
            return null;
        }
    }
}

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.

Kotlin

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