Preferences DataStore

1. Avant de commencer

Dans les précédents ateliers de programmation, vous avez appris à enregistrer des données dans une base de données SQLite à l'aide de Room, une couche d'abstraction de base de données. Cet atelier de programmation présente Jetpack DataStore. Basé sur les coroutines Kotlin et Flow, DataStore propose deux implémentations différentes : Proto DataStore, qui stocke des objets typés, et Preferences DataStore, qui stocke des paires clé/valeur.

Dans cet atelier de programmation, vous apprendrez à utiliser Preferences DataStore. Proto DataStore n'entre pas dans le cadre de cet atelier de programmation.

Conditions préalables

  • Vous maîtrisez les composants d'architecture Android ViewModel, LiveData et Flow, et vous savez comment utiliser ViewModelProvider.Factory pour instancier ViewModel.
  • Vous maîtrisez les principes de base de la simultanéité.
  • Vous êtes capable d'utiliser des coroutines pour des tâches de longue durée.

Points abordés

  • En quoi consiste DataStore, et pourquoi et quand l'utiliser.
  • Comment ajouter Preferences DataStore à votre application.

Ce dont vous avez besoin

  • Code de démarrage de l'application Words (même code de solution d'application Words que celui d'un atelier de programmation précédent).
  • Un ordinateur sur lequel est installé Android Studio

Télécharger le code de démarrage pour cet atelier de programmation

Dans cet atelier de programmation, vous allez étendre les fonctionnalités de l'application Words à partir du code de solution précédent. Le code de démarrage peut contenir du code que vous avez déjà étudié lors d'ateliers de programmation précédents.

Pour télécharger le code de cet atelier de programmation à partir de GitHub et l'ouvrir dans Android Studio, procédez comme suit :

  1. Lancez Android Studio.
  2. Dans la fenêtre Bienvenue dans Android Studio, cliquez sur Obtenir à partir de VCS.

61c42d01719e5b6d.png

  1. Dans la boîte de dialogue Obtenir à partir du contrôle des versions, assurez-vous que Git est sélectionné pour Contrôle des versions.

9284cfbe17219bbb.png

  1. Collez l'URL du code fourni dans le champ URL.
  2. Vous pouvez également indiquer une valeur autre que la suggestion par défaut dans le champ Répertoire.

5ddca7dd0d914255.png

  1. Cliquez sur Clone (Cloner). Android Studio commence à récupérer votre code.
  2. Attendez qu'Android Studio s'ouvre.
  3. Sélectionnez le module approprié pour le code de solution, d'application ou de démarrage de votre atelier de programmation.

2919fe3e0c79d762.png

  1. Cliquez sur le bouton Exécuter 8de56cba7583251f.png pour créer et exécuter votre code.

2. Présentation de l'application de démarrage

L'application Words se compose de deux écrans : le premier affiche les lettres que l'utilisateur peut sélectionner et le deuxième affiche une liste de mots commençant par les lettres sélectionnées.

Cette application propose une option de menu permettant à l'utilisateur de basculer entre les dispositions en grille ou en liste pour les lettres.

  1. Téléchargez le code de démarrage, ouvrez-le dans Android Studio et exécutez l'application. Les lettres sont affichées de façon linéaire.
  2. Appuyez sur l'option de menu en haut à droite. La disposition "Grille" s'affiche.
  3. Quittez l'application, puis relancez-la. Pour ce faire, utilisez les options Arrêter "application" f782441b99bdd0a4.png et Exécuter "application" d203bd07cbce5954.png d'Android Studio. Notez que lorsque l'application est relancée, les caractères sont affichés selon une disposition linéaire et non dans une grille.

La sélection effectuée par l'utilisateur n'est pas conservée. Cet atelier de programmation vous explique comment résoudre ce problème.

Objectifs de l'atelier

  • Dans cet atelier de programmation, vous allez apprendre à utiliser Preferences DataStore pour conserver le paramètre de mise en page dans DataStore.

3. Présentation de Preferences DataStore

Preferences DataStore est idéal pour les petits ensembles de données simples ; par exemple, pour le stockage d'identifiants, du paramètre du mode sombre et de la taille de police. DataStore n'est pas adapté aux ensembles de données complexes, comme la liste d'inventaire d'une épicerie en ligne ou une base de données d'étudiants. Si vous avez besoin de stocker des ensembles de données volumineux ou complexes, envisagez d'utiliser Room plutôt que DataStore.

La bibliothèque Jetpack DataStore permet de créer une API simple, sécurisée et asynchrone pour le stockage de données. Elle fournit deux implémentations différentes : Preferences DataStore et Proto DataStore. Bien que Preferences DataStore et Proto DataStore permettent tous deux d'enregistrer des données, ils ne procèdent pas de la même manière :

  • Preferences DataStore accède aux données et les stocke en fonction de clés, sans définir de schéma (modèle de données) au préalable.
  • Proto DataStore définit le schéma à l'aide de tampons de protocole. L'utilisation de tampons de protocole vous permet de conserver des données fortement typées. Ils sont plus rapides, plus petits, plus simples et moins ambigus que le format XML et d'autres formats de données similaires.

Room et Datastore : quand les utiliser ?

Si votre application doit stocker des données volumineuses/complexes dans un format structuré tel que SQL, pensez à utiliser Room. Cependant, si vous ne devez stocker que de petites quantités de données ou des données simples pouvant être enregistrées dans des paires clé-valeur, DataStore constitue le choix idéal.

Proto DataStore et Preferences DataStore : quand les utiliser ?

Proto DataStore est une solution sécurisée et efficace, mais qui nécessite une configuration. Si les données de votre application sont suffisamment simples pour être enregistrées dans des paires clé-valeur, Preferences DataStore est plus adapté, car il est beaucoup plus facile à configurer.

Ajouter Preferences DataStore comme dépendance

La première étape de l'intégration d'un datastore dans votre application consiste à l'ajouter en tant que dépendance.

  1. Dans build.gradle(Module: Words.app), ajoutez la dépendance suivante :
implementation "androidx.datastore:datastore-preferences:1.0.0"
implementation "androidx.lifecycle:lifecycle-livedata-ktx:2.3.1"

4. Créer un Preferences DataStore

  1. Ajoutez un package nommé data et créez une classe Kotlin nommée SettingsDataStore à l'intérieur de ce package.
  2. Ajoutez un paramètre constructeur à la classe SettingsDataStore de type Context.
class SettingsDataStore(context: Context) {}
  1. En dehors de la classe SettingsDataStore, déclarez un private const val nommé LAYOUT_PREFERENCES_NAME et attribuez-lui la valeur de chaîne layout_preferences. C'est le nom du Preferences Datastore que vous allez instancier à l'étape suivante.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
  1. Toujours en dehors de la classe, créez une instance DataStore à l'aide du délégué preferencesDataStore. Comme vous utilisez Preferences Datastore, vous devez transmettre Preferences en tant que type de datastore. Définissez également le datastore name sur LAYOUT_PREFERENCES_NAME.

Voici le code fini :

private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"

// Create a DataStore instance using the preferencesDataStore delegate, with the Context as
// receiver.
private val Context.dataStore : DataStore<Preferences> by preferencesDataStore(
   name = LAYOUT_PREFERENCES_NAME
)

5. Implémenter la classe SettingsDataStore

Comme nous l'avons vu précédemment, Preferences DataStore stocke les données dans des paires clé-valeur. Au cours de cette étape, vous allez définir les clés nécessaires pour stocker le paramètre de mise en page, ainsi que les fonctions d'écriture et de lecture dans Preferences DataStore.

Fonctions de type de clé

Preferences DataStore n'utilise pas de schéma prédéfini comme Room. Il utilise les fonctions de type de clé correspondantes pour définir une clé pour chaque valeur que vous stockez dans l'instance DataStore<Preferences>. Par exemple, pour définir une clé pour une valeur int, utilisez intPreferencesKey(). Pour une valeur string, utilisez stringPreferencesKey(). En règle générale, ces noms de fonction sont précédés du type de données dans lequel vous souhaitez stocker la clé.

Implémentez ce qui suit dans la classe data\SettingsDataStore :

  1. Pour implémenter la classe SettingsDataStore, la première étape consiste à créer une clé qui stocke une valeur booléenne indiquant si le paramètre utilisateur est une mise en page linéaire. Créez une propriété de classe private nommée IS_LINEAR_LAYOUT_MANAGER et initialisez-la à l'aide de booleanPreferencesKey() en transmettant le nom de clé is_linear_layout_manager comme paramètre de fonction.
private val IS_LINEAR_LAYOUT_MANAGER = booleanPreferencesKey("is_linear_layout_manager")

Écrire dans Preferences DataStore

Il est maintenant temps d'utiliser votre clé et de stocker le paramètre de mise en page booléen dans DataStore. Preferences DataStore fournit une fonction de suspension edit() qui met à jour les données dans DataStore de manière transactionnelle. Le paramètre de transformation de la fonction accepte un bloc de code dans lequel vous pouvez mettre à jour les valeurs si nécessaire. Tout le code du bloc de transformation est considéré comme une seule transaction. En arrière-plan, le travail transactionnel est déplacé vers Dispacter.IO. Pensez donc à suspendre (suspend) votre fonction lors de l'appel de la fonction edit().

  1. Créez une fonction suspend appelée saveLayoutToPreferencesStore() qui accepte deux paramètres : le paramètre de mise en page booléen et le Context.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {

}
  1. Implémentez la fonction ci-dessus, appelez dataStore.edit() et transmettez un bloc de code pour stocker la nouvelle valeur.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
   context.dataStore.edit { preferences ->
       preferences[IS_LINEAR_LAYOUT_MANAGER] = isLinearLayoutManager
   }
}

Lire à partir de Preferences DataStore

Preferences DataStore présente les données stockées dans un Flow<Preferences> qui est émis chaque fois qu'une préférence est modifiée. Vous ne souhaitez pas exposer l'intégralité de l'objet Preferences, mais uniquement la valeur Boolean. Pour ce faire, le Flow<Preferences> est mappé et nous obtenons la valeur Boolean qui vous intéresse.

  1. Exposez un preferenceFlow: Flow<UserPreferences>, construit à partir de dataStore.data: Flow<Preferences>, et mappez-le pour récupérer la préférence Boolean. Comme le datastore est vide lors de la première exécution, renvoyez true par défaut.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }
  1. Ajoutez les importations suivantes si elles ne sont pas automatiquement importées :
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map

Gestion des exceptions

Lorsque DataStore lit et écrit des données à partir de fichiers, des IOExceptions peuvent se produire lors de l'accès aux données. Vous pouvez gérer ces exceptions à l'aide de l'opérateur catch().

  1. SharedPreferences DataStore génère une IOException si une erreur se produit lors de la lecture des données. Dans la déclaration preferenceFlow, avant map(), utilisez l'opérateur catch() pour intercepter IOException et émettre emptyPreferences(). Pour faire simple, étant donné qu'aucun autre type d'exception n'est attendu ici, si un type différent est généré, exécutez une nouvelle génération.
val preferenceFlow: Flow<Boolean> = context.dataStore.data
   .catch {
       if (it is IOException) {
           it.printStackTrace()
           emit(emptyPreferences())
       } else {
           throw it
       }
   }
   .map { preferences ->
       // On the first run of the app, we will use LinearLayoutManager by default
       preferences[IS_LINEAR_LAYOUT_MANAGER] ?: true
   }

Votre classe data\SettingsDataStore est prête à l'emploi !

6. Utiliser la classe SettingsDataStore

Dans cette tâche, vous allez utiliser SettingsDataStore dans votre classe LetterListFragment. Vous allez associer un observateur au paramètre de mise en page et mettre à jour l'interface utilisateur en conséquence.

Implémentez les étapes suivantes dans LetterListFragment :

  1. Déclarez une variable de classe private nommée SettingsDataStore, de type SettingsDataStore. Définissez cette variable sur lateinit, car vous l'initialiserez plus tard.
private lateinit var SettingsDataStore: SettingsDataStore
  1. À la fin de la fonction onViewCreated(), initialisez la nouvelle variable et transmettez requireContext() au constructeur SettingsDataStore.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
}

Lire et observer les données

  1. Dans LetterListFragment, dans la méthode onViewCreated(), sous l'initialisation SettingsDataStore, convertissez preferenceFlow en Livedata à l'aide de asLiveData(). Associez un observateur et transmettez viewLifecycleOwner en tant que propriétaire.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
  1. Dans l'observateur, attribuez le nouveau paramètre de mise en page à la variable isLinearLayoutManager. Appelez la fonction chooseLayout() pour mettre à jour la mise en page RecyclerView.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })

La fonction onViewCreated() terminée devrait ressembler à ceci :

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   recyclerView = binding.recyclerView
   // Initialize SettingsDataStore
   SettingsDataStore = SettingsDataStore(requireContext())
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           isLinearLayoutManager = value
           chooseLayout()
   })
}

Écrire le paramètre de mise en page dans DataStore

La dernière étape consiste à écrire le paramètre de mise en page dans Preferences DataStore lorsque l'utilisateur appuie sur l'option de menu. L'écriture de données dans Preferences DataStore doit être effectuée de manière asynchrone dans une coroutine. Pour effectuer cette opération dans un fragment, utilisez la méthode CoroutineScope appelée LifecycleScope.

LifecycleScope

Les composants compatibles avec le cycle de vie, tels que les fragments, fournissent le plus haut niveau de coroutine pour les portées logiques de votre application, ainsi qu'une couche d'interopérabilité avec LiveData. Un LifecycleScope est défini pour chaque objet Lifecycle. Toute coroutine lancée au sein de cette portée est annulée lorsque le propriétaire de Lifecycle est détruit.

  1. Dans LetterListFragment, dans la fonction onOptionsItemSelected(), à la fin du cas R.id.action_switch_layout, lancez la coroutine à l'aide de lifecycleScope. Dans le bloc launch, appelez saveLayoutToPreferencesStore() en transmettant isLinearLayoutManager et context.
override fun onOptionsItemSelected(item: MenuItem): Boolean {
   return when (item.itemId) {
       R.id.action_switch_layout -> {
           ...
           // Launch a coroutine and write the layout setting in the preference Datastore
           lifecycleScope.launch {
       SettingsDataStore.saveLayoutToPreferencesStore(isLinearLayoutManager, requireContext())
           }
           ...

           return true
       }
  1. Exécutez l'application. Cliquez sur l'option de menu pour modifier la mise en page de l'application.

  1. Testez maintenant la persistance de Preferences DataStore. Définissez la mise en page de l'application sur Grille. Quittez l'application, puis relancez-la. Pour ce faire, utilisez les options Arrêter "application" f782441b99bdd0a4.png et Exécuter "application" d203bd07cbce5954.png d'Android Studio.

cd2c31f27dfb5157.png

Une fois l'application relancée, les caractères sont affichés sous la forme d'une grille et non selon une disposition linéaire. Votre application enregistre correctement le paramètre de mise en page sélectionné par l'utilisateur.

Bien que les lettres s'affichent désormais dans une grille, l'icône de menu n'est pas mise à jour correctement. Nous verrons ensuite comment résoudre ce problème.

7. Corriger le bug de l'icône de menu

Ce bug se produit car, dans onViewCreated(), la mise en page RecyclerView est mise à jour en fonction du paramètre de mise en page et non de l'icône de menu. Vous pouvez résoudre ce problème en redessinant le menu et en mettant à jour la mise en page RecyclerView.

Redessiner le menu d'options

Une fois le menu créé, il n'est pas redessiné à chaque image, car cela serait redondant. La fonction invalidateOptionsMenu() indique à Android de redessiner le menu d'options.

Vous pouvez appeler cette fonction lorsque vous modifiez un élément du menu Options ; par exemple, lorsque vous ajoutez ou supprimez un élément de menu, ou modifiez le texte ou l'icône du menu. Dans le cas présent, l'icône du menu a été modifiée. L'appel de cette méthode déclare que le menu Options a été modifié et doit donc être recréé. La méthode onCreateOptionsMenu(android.view.Menu) sera appelée la prochaine fois qu'elle devra être affichée.

  1. Dans LetterListFragment, à l'intérieur de onViewCreated(), à la fin de l'observateur preferenceFlow, sous l'appel de chooseLayout(). Redessinez le menu en appelant invalidateOptionsMenu() sur activity.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
   ...
   SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
           ...
           // Redraw the menu
           activity?.invalidateOptionsMenu()
   })
}
  1. Exécutez à nouveau l'application et modifiez la mise en page.
  2. Quittez l'application, puis relancez-la. Notez que l'icône du menu est maintenant mise à jour correctement.

1c8cf63c8d175aad.png

Félicitations ! Vous avez ajouté Preferences DataStore à votre application pour enregistrer la sélection de l'utilisateur.

8. Code de solution

Le code de solution de cet atelier de programmation figure dans le projet et le module ci-dessous.

9. Résumé

  • DataStore dispose d'une API totalement asynchrone basée sur des coroutines Kotlin et Flow, ce qui garantit la cohérence des données.
  • Jetpack DataStore est une solution de stockage de données qui vous permet de stocker des paires clé-valeur ou des objets typés avec des tampons de protocole.
  • DataStore propose deux implémentations différentes : Preferences DataStore et Proto DataStore.
  • Preferences DataStore n'utilise pas de schéma prédéfini.
  • Preferences DataStore utilise la fonction de type de clé correspondante pour définir une clé pour chaque valeur qui doit être stockée dans l'instance DataStore<Preferences>. Par exemple, pour définir une clé pour une valeur int, utilisez intPreferencesKey().
  • Preferences DataStore fournit une fonction edit() qui met à jour les données dans DataStore de manière transactionnelle.

10. En savoir plus

Blog

Stocker des données avec Jetpack DataStore, de préférence