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
etFlow
, et vous savez comment utiliserViewModelProvider.Factory
pour instancierViewModel
. - 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 :
- Lancez Android Studio.
- Dans la fenêtre Bienvenue dans Android Studio, cliquez sur Obtenir à partir de VCS.
- 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.
- Collez l'URL du code fourni dans le champ URL.
- Vous pouvez également indiquer une valeur autre que la suggestion par défaut dans le champ Répertoire.
- Cliquez sur Clone (Cloner). Android Studio commence à récupérer votre code.
- Attendez qu'Android Studio s'ouvre.
- Sélectionnez le module approprié pour le code de solution, d'application ou de démarrage de votre atelier de programmation.
- Cliquez sur le bouton Exécuter 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.
- 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.
- Appuyez sur l'option de menu en haut à droite. La disposition "Grille" s'affiche.
- Quittez l'application, puis relancez-la. Pour ce faire, utilisez les options Arrêter "application" et Exécuter "application" 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.
- 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
- Ajoutez un package nommé
data
et créez une classe Kotlin nomméeSettingsDataStore
à l'intérieur de ce package. - Ajoutez un paramètre constructeur à la classe
SettingsDataStore
de typeContext
.
class SettingsDataStore(context: Context) {}
- En dehors de la classe
SettingsDataStore
, déclarez unprivate const val
nomméLAYOUT_PREFERENCES_NAME
et attribuez-lui la valeur de chaînelayout_preferences
. C'est le nom du Preferences Datastore que vous allez instancier à l'étape suivante.
private const val LAYOUT_PREFERENCES_NAME = "layout_preferences"
- 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 transmettrePreferences
en tant que type de datastore. Définissez également le datastorename
surLAYOUT_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
:
- 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 classeprivate
nomméeIS_LINEAR_LAYOUT_MANAGER
et initialisez-la à l'aide debooleanPreferencesKey()
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()
.
- Créez une fonction
suspend
appeléesaveLayoutToPreferencesStore()
qui accepte deux paramètres : le paramètre de mise en page booléen et leContext
.
suspend fun saveLayoutToPreferencesStore(isLinearLayoutManager: Boolean, context: Context) {
}
- 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.
- Exposez un
preferenceFlow: Flow<UserPreferences>
, construit à partir dedataStore.data: Flow<Preferences>
, et mappez-le pour récupérer la préférenceBoolean
. Comme le datastore est vide lors de la première exécution, renvoyeztrue
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
}
- 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()
.
- SharedPreferences DataStore génère une
IOException
si une erreur se produit lors de la lecture des données. Dans la déclarationpreferenceFlow
, avantmap()
, utilisez l'opérateurcatch()
pour intercepterIOException
et émettreemptyPreferences()
. 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
:
- Déclarez une variable de classe
private
nomméeSettingsDataStore
, de typeSettingsDataStore
. Définissez cette variable surlateinit
, car vous l'initialiserez plus tard.
private lateinit var SettingsDataStore: SettingsDataStore
- À la fin de la fonction
onViewCreated()
, initialisez la nouvelle variable et transmettezrequireContext()
au constructeurSettingsDataStore
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
// Initialize SettingsDataStore
SettingsDataStore = SettingsDataStore(requireContext())
}
Lire et observer les données
- Dans
LetterListFragment
, dans la méthodeonViewCreated()
, sous l'initialisationSettingsDataStore
, convertissezpreferenceFlow
enLivedata
à l'aide deasLiveData
()
. Associez un observateur et transmettezviewLifecycleOwner
en tant que propriétaire.
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { })
- Dans l'observateur, attribuez le nouveau paramètre de mise en page à la variable
isLinearLayoutManager
. Appelez la fonctionchooseLayout()
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.
- Dans
LetterListFragment
, dans la fonctiononOptionsItemSelected()
, à la fin du casR.id.
action_switch_layout
, lancez la coroutine à l'aide delifecycleScope
. Dans le bloclaunch
, appelezsaveLayoutToPreferencesStore()
en transmettantisLinearLayoutManager
etcontext
.
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
}
- Exécutez l'application. Cliquez sur l'option de menu pour modifier la mise en page de l'application.
- 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" et Exécuter "application" d'Android Studio.
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.
- Dans
LetterListFragment
, à l'intérieur deonViewCreated()
, à la fin de l'observateurpreferenceFlow
, sous l'appel dechooseLayout()
. Redessinez le menu en appelantinvalidateOptionsMenu()
suractivity
.
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
...
SettingsDataStore.preferenceFlow.asLiveData().observe(viewLifecycleOwner, { value ->
...
// Redraw the menu
activity?.invalidateOptionsMenu()
})
}
- Exécutez à nouveau l'application et modifiez la mise en page.
- Quittez l'application, puis relancez-la. Notez que l'icône du menu est maintenant mise à jour correctement.
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 valeurint
, utilisezintPreferencesKey()
. - Preferences DataStore fournit une fonction
edit()
qui met à jour les données dansDataStore
de manière transactionnelle.
10. En savoir plus
DataStore
guide
- Documentation de référence sur DataStore
- Préférences
- android.datastore.preferences.core