1. Introduction
Qu'est-ce que DataStore ?
DataStore est une nouvelle solution de stockage de données améliorée, destinée à remplacer SharedPreferences. Basé sur les coroutines Kotlin et Kotlin Flow, DataStore propose deux implémentations différentes : Proto DataStore, qui vous permet de stocker des objets typés (grâce à des tampons de protocole) et Preferences DataStore, qui stocke des paires clé/valeur. Les données sont stockées de manière asynchrone, cohérente et transactionnelle, permettant ainsi de pallier certains des inconvénients de SharedPreferences.
Points abordés
- En quoi consiste DataStore et pourquoi l'utiliser
- Ajouter DataStore à votre projet
- Différences entre Preferences DataStore et Proto DataStore, et leurs avantages respectifs
- Utiliser Proto DataStore
- Migrer de SharedPreferences vers Proto DataStore
Objectif de cet atelier
Dans cet atelier de programmation, vous allez commencer avec un exemple d'application affichant une liste de tâches pouvant être filtrées par état d'avancement et triées par priorité et par date d'échéance.
L'indicateur booléen du filtre Show completed tasks (Afficher les tâches effectuées) est enregistré en mémoire. L'ordre de tri est conservé sur le disque à l'aide d'un objet SharedPreferences
.
Étant donné qu'il existe deux implémentations différentes pour DataStore, Preferences DataStore et Proto DataStore, vous apprendrez à utiliser Proto DataStore en effectuant les tâches suivantes dans chaque implémentation :
- Conserver le filtre d'état d'avancement dans DataStore
- Migrer l'ordre de tri de SharedPreferences vers DataStore
Nous vous recommandons de suivre également l'atelier de programmation Preferences DataStore afin de mieux comprendre la différence entre les deux.
Prérequis
- Android Studio Arctic Fox.
- Bonne connaissance des composants d'architecture LiveData, ViewModel et View Binding, ainsi que de l'architecture suggérée dans le Guide de l'architecture des applications
- Bonne connaissance des coroutines et de Kotlin Flow
Pour une présentation des composants d'architecture, consultez notre atelier de programmation dans Room. Pour plus d'informations sur Flow, consultez l'atelier de programmation Coroutines avancées avec Kotlin Flow et LiveData.
2. Configuration
Au cours de cette étape, vous téléchargerez l'intégralité du code de cet atelier de programmation, puis exécuterez un exemple d'application simple.
Pour vous aider à démarrer le plus rapidement possible, nous avons préparé un projet de démarrage.
Si git est installé, vous pouvez simplement exécuter la commande ci-dessous. Pour vérifier si git est installé, saisissez git --version
dans le terminal ou la ligne de commande, et vérifiez qu'il fonctionne correctement.
git clone https://github.com/googlecodelabs/android-datastore
L'état initial se trouve dans la branche master
. Le code de la solution se trouve dans la branche proto_datastore
.
Si vous n'avez pas git, vous pouvez cliquer sur le bouton suivant pour télécharger tout le code de cet atelier de programmation :
- Décompressez le code, puis ouvrez le projet dans Android Studio Arctic Fox.
- Exécutez la configuration d'exécution de l'application sur un appareil ou un émulateur.
L'application s'exécute et affiche la liste des tâches :
3. Présentation du projet
L'application vous permet d'afficher une liste de tâches. Chaque tâche possède les propriétés suivantes : nom, état d'exécution, priorité et date d'échéance.
Pour simplifier le code que nous devons utiliser, l'application vous permet d'effectuer ces deux actions uniquement :
- Activer/désactiver l'option Show completed tasks (Afficher les tâches exécutées). Les tâches sont masquées par défaut.
- Trier les tâches par ordre de priorité, par date d'échéance, ou par date d'échéance et ordre de priorité
L'application suit l'architecture recommandée dans le Guide de l'architecture des applications. Voici le contenu de chaque package :
data
- La classe de modèle
Task
. - La classe
TasksRepository
, chargée de fournir les tâches. Pour souci de simplicité, cette classe renvoie des données codées en dur et les affiche via unFlow
pour représenter un scénario plus réaliste. - La classe
UserPreferencesRepository
: contientSortOrder
, défini en tant queenum
. L'ordre de tri actuel est enregistré dans SharedPreferences en tant queString
, selon le nom de la valeur d'énumération. Elle propose des méthodes synchrones pour enregistrer et obtenir l'ordre de tri.
ui
- Classes associées à l'affichage d'une
Activity
avec unRecyclerView
- La classe
TasksViewModel
est responsable de la logique de l'interface utilisateur
TasksViewModel
contient tous les éléments nécessaires à la création des données à afficher dans l'interface utilisateur : liste des tâches, indicateurs showCompleted
et sortOrder
, le tout encapsulé dans un objet TasksUiModel
. Chaque fois que l'une de ces valeurs change, il nous faut créer un nouveau TasksUiModel
. Pour cela, nous combinons trois éléments :
- Un élément
Flow<List<Task>>
, récupéré à partir deTasksRepository
- Un élément
MutableStateFlow<Boolean>
, contenant le dernier indicateurshowCompleted
, qui n'est conservé que dans la mémoire. - Un élément
MutableStateFlow<SortOrder>
, contenant la dernière valeursortOrder
.
Pour nous assurer que nous effectuons correctement la mise à jour de l'interface utilisateur, uniquement lorsque l'activité est lancée, nous affichons un LiveData<TasksUiModel>
.
Nous rencontrons quelques problèmes avec notre code :
- Nous bloquons le thread UI sur les opérations d'E/S du disque lors de l'initialisation de
UserPreferencesRepository.sortOrder
. Cela peut entraîner des à-coups dans l'interface utilisateur. - L'indicateur
showCompleted
n'est conservé que dans la mémoire. Cela signifie qu'il est réinitialisé chaque fois que l'utilisateur ouvre l'application. À l'instar deSortOrder
, il faut conserver cet élément pour qu'il survive à la fermeture de l'application. - Nous utilisons actuellement SharedPreferences pour conserver des données, mais nous gardons un
MutableStateFlow
dans la mémoire, que nous modifions manuellement afin d'être avertis des changements. Cela peut rapidement poser problème si la valeur est modifiée ailleurs dans l'application. - Dans
UserPreferencesRepository
, nous présentons deux méthodes permettant de mettre à jour l'ordre de tri :enableSortByDeadline()
etenableSortByPriority()
. Ces deux méthodes reposent sur la valeur actuelle de l'ordre de tri, mais si l'une est appelée avant la fin de l'autre, nous obtenons une valeur finale incorrecte. En particulier, ces méthodes peuvent générer des à-coups dans l'interface utilisateur ou des violations du mode strict, car elles sont appelées dans le thread UI.
Bien que les indicateurs showCompleted
et sortOrder
soient des préférences utilisateur, ils sont actuellement représentés comme deux objets distincts. L'un de nos objectifs consiste donc à regrouper ces deux indicateurs en une même classe UserPreferences
.
Découvrons comment utiliser DataStore pour résoudre ces problèmes.
4. DataStore : principes de base
Vous aurez peut-être souvent besoin de stocker des ensembles de données simples ou petits. Vous avez peut-être déjà utilisé SharedPreferences dans le passé, mais cette API comporte également un certain nombre d'inconvénients. La bibliothèque Jetpack DataStore vise à résoudre ces problèmes en créant une API simple, plus sécurisée et asynchrone pour le stockage des données. Elle propose deux implémentations différentes :
- Preferences DataStore
- Proto DataStore
Fonctionnalité | SharedPreferences | Preferences DataStore | Proto DataStore |
API Async | ✅ (uniquement pour la lecture de valeurs modifiées, via l'écouteur) | ✅ (via | ✅ (via |
API synchrone | ✅ (mais appel non sécurisé via un thread UI) | ❌ | ❌ |
Appel sécurisé via un thread UI | ❌(1) | ✅ (le travail est déplacé vers | ✅ (le travail est déplacé vers |
Peut signaler des erreurs | ❌ | ✅ | ✅ |
Protection contre les exceptions d'exécution | ❌(2) | ✅ | ✅ |
Comporte une API transactionnelle dotée d'une garantie de cohérence forte | ❌ | ✅ | ✅ |
Gestion de la migration des données | ❌ | ✅ | ✅ |
Sécurité du type | ❌ | ❌ | ✅ avec tampons de protocole |
(1) SharedPreferences dispose d'une API synchrone dont l'appel sur le thread UI peut sembler sûr, mais qui effectue en réalité des opérations d'E/S sur le disque. De plus, apply()
bloque le thread UI sur fsync()
. Les appels fsync()
en attente sont déclenchés chaque fois qu'un service démarre ou s'arrête, et chaque fois qu'une activité démarre ou s'arrête quelque part dans votre application. Le thread UI est bloqué pour les appels fsync()
en attente planifiés par apply()
, et deviennent souvent une source d'ANR.
(2) SharedPreferences génère des erreurs d'analyse sous la forme d'exceptions d'exécution.
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 :
- Comme SharedPreferences, Preference DataStore accède aux données en fonction de clés, sans définir de schéma au préalable.
- Proto DataStore définit le schéma à l'aide de tampons de protocole. L'utilisation de tampons de protocole permet de conserver des données très typées. Ils sont plus rapides, plus petits, plus simples et moins ambigus que le format XML et autres formats de données similaires. Bien que Proto DataStore nécessite d'apprendre un nouveau mécanisme de sérialisation, nous pensons que l'avantage offert par Proto DataStore quant aux données très typées en vaut la peine.
Room et DataStore
Si vous avez besoin d'effectuer des mises à jour partielles, d'assurer l'intégrité des référentiels ou de disposer d'ensembles de données volumineux ou complexes, nous vous recommandons d'utiliser Room au lieu de DataStore. DataStore est idéal pour les ensembles de données simples ou petits, et n'accepte pas les mises à jour partielles ni l'intégrité des référentiels.
5. Proto DataStore : présentation
L'un des inconvénients de SharedPreferences et de Preferences DataStore est qu'il n'existe aucun moyen de définir un schéma ni de garantir l'accessibilité aux clés avec le type approprié. Proto DataStore résout ce problème en utilisant des tampons de protocole pour définir le schéma. L'utilisation de tampons de protocole permet à DataStore de savoir quels types sont stockés et de les fournir simplement, ce qui évite d'avoir à utiliser des clés.
Voyons maintenant ce que sont les tampons de protocole, comment ajouter Proto DataStore et des tampons de protocole au projet, comment utiliser les tampons de protocole avec Proto DataStore et comment migrer SharedPreferences vers DataStore.
Ajouter des dépendances
Pour travailler avec Proto DataStore et faire en sorte qu'un tampon de protocole génère du code pour notre schéma, nous allons apporter plusieurs modifications au fichier build.gradle :
- Ajouter le plug-in des tampons de protocole
- Ajouter les dépendances des tampons de protocole et de Proto DataStore
- Configurer les tampons de protocole
plugins {
...
id "com.google.protobuf" version "0.8.17"
}
dependencies {
implementation "androidx.datastore:datastore-core:1.0.0"
implementation "com.google.protobuf:protobuf-javalite:3.18.0"
...
}
protobuf {
protoc {
artifact = "com.google.protobuf:protoc:3.14.0"
}
// Generates the java Protobuf-lite code for the Protobufs in this project. See
// https://github.com/google/protobuf-gradle-plugin#customizing-protobuf-compilation
// for more information.
generateProtoTasks {
all().each { task ->
task.builtins {
java {
option 'lite'
}
}
}
}
}
6. Définir et utiliser des objets tampons de protocoles
Les tampons de protocole sont un mécanisme de sérialisation des données structurées. Vous définissez la manière dont vous souhaitez que vos données soient structurées une seule fois, puis le compilateur génère du code source pour écrire et lire facilement les données structurées.
Créer le fichier .proto
Vous définissez votre schéma dans un fichier .proto. Dans notre atelier de programmation, nous avons deux préférences utilisateur, show_completed
et sort_order
, qui sont actuellement représentés comme deux objets différents. L'un de nos objectifs consiste donc à regrouper ces deux indicateurs en une même classe UserPreferences
stockée dans DataStore. Au lieu de définir cette classe dans Kotlin, nous allons la définir dans un schéma de tampons de protocole.
Consultez le Guide du langage proto pour obtenir des informations détaillées sur la syntaxe. Dans cet atelier de programmation, nous n'aborderons que les types dont nous aurons besoin.
Créez un fichier nommé user_prefs.proto
dans le répertoire app/src/main/proto
. Si vous ne voyez pas cette structure de dossiers, passez en vue Projet. Dans les tampons de protocole, chaque structure est définie à l'aide d'un mot clé message
, et chaque membre de la structure est défini dans le message en fonction du type et du nom. Chaque structure se voit attribuer un ordre basé sur 1. Définissons maintenant un message UserPreferences
qui, pour l'instant, comporte simplement une valeur booléenne appelée show_completed
.
syntax = "proto3";
option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;
message UserPreferences {
// filter for showing / hiding completed tasks
bool show_completed = 1;
}
Créer le sérialiseur
Pour indiquer à DataStore comment lire et écrire le type de données défini dans le fichier .proto, nous devons mettre en place un sérialisateur. Le sérialiseur définit également la valeur par défaut à afficher en l'absence d'informations sur le disque. Créez un fichier nommé UserPreferencesSerializer
dans le package data
:
object UserPreferencesSerializer : Serializer<UserPreferences> {
override val defaultValue: UserPreferences = UserPreferences.getDefaultInstance()
override suspend fun readFrom(input: InputStream): UserPreferences {
try {
return UserPreferences.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Cannot read proto.", exception)
}
}
override suspend fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}
7. Conserver des données dans Proto DataStore
Créer le DataStore
L'indicateur showCompleted
est conservé en mémoire, dans TasksViewModel
, mais devrait être stocké dans UserPreferencesRepository
, dans une instance DataStore.
Pour créer une instance DataStore, nous utilisons le délégué dataStore
, avec le Context
comme destinataire. Ce délégué comporte deux paramètres obligatoires :
- Le nom du fichier sur lequel DataStore agira
- Le sérialiseur pour le type utilisé avec DataStore (dans notre cas :
UserPreferencesSerializer
)
Dans cet atelier de programmation, nous allons réaliser cette opération dans TasksActivity
, par souci de simplicité :
private const val USER_PREFERENCES_NAME = "user_preferences"
private const val DATA_STORE_FILE_NAME = "user_prefs.pb"
private const val SORT_ORDER_KEY = "sort_order"
private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
fileName = DATA_STORE_FILE_NAME,
serializer = UserPreferencesSerializer
)
Le délégué dataStore
permet de s'assurer que nous n'avons qu'une seule instance de DataStore portant ce nom dans notre application. Actuellement, UserPreferencesRepository
est implémenté comme Singleton, car il contient sortOrderFlow
et évite qu'il ne soit lié au cycle de vie de TasksActivity
. Comme UserPreferenceRepository
n'utilisera que les données de DataStore, et ne créera pas d'objets ni n'en contiendra de nouveaux, nous pouvons dès à présent supprimer l'implémentation du Singleton :
- Supprimez
companion object
. - Rendez
constructor
public.
UserPreferencesRepository
devrait recevoir une instance DataStore
comme paramètre de constructeur. Pour le moment, nous pouvons laisser Context
comme paramètre, car SharedPreferences en a besoin. Toutefois, nous le supprimerons plus tard.
class UserPreferencesRepository(
private val userPreferencesStore: DataStore<UserPreferences>,
context: Context
) { ... }
Mettons à jour la construction de UserPreferencesRepository
dans TasksActivity
et transmettons dataStore
:
viewModel = ViewModelProvider(
this,
TasksViewModelFactory(
TasksRepository,
UserPreferencesRepository(dataStore, this)
)
).get(TasksViewModel::class.java)
Lire des données depuis Proto DataStore
Proto DataStore expose les données stockées dans un Flow<UserPreferences>
. Nous allons maintenant créer une valeur publique userPreferencesFlow: Flow<UserPreferences>
qui se voit attribuer dataStore.data
:
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
Gérer les exceptions lors de la lecture des données
Des exceptions IOException
sont générées lorsqu'une erreur se produit au cours de la lecture des données à partir d'un fichier par DataStore. Pour résoudre ces problèmes, utilisez la transformation Flow catch
et consignez l'erreur :
private val TAG: String = "UserPreferencesRepo"
val userPreferencesFlow: Flow<UserPreferences> = dataStore.data
.catch { exception ->
// dataStore.data throws an IOException when an error is encountered when reading data
if (exception is IOException) {
Log.e(TAG, "Error reading sort order preferences.", exception)
emit(UserPreferences.getDefaultInstance())
} else {
throw exception
}
}
Écrire des données dans Proto DataStore
Pour écrire des données, DataStore propose une fonction DataStore.updateData()
de suspension, où nous obtenons l'état actuel des UserPreferences
en tant que paramètre. Pour le mettre à jour, nous devons transformer l'objet des préférences en compilateur, définir la nouvelle valeur, puis définir les nouvelles préférences.
updateData()
met à jour les données de manière transactionnelle en une opération atomique de lecture-écriture-modification. La coroutine se termine une fois que les données sont conservées sur le disque.
Créons à présent une fonction de suspension permettant de mettre à jour la propriété showCompleted
de UserPreferences
, appelée updateShowCompleted()
, qui appelle dataStore.updateData()
et définit la nouvelle valeur :
suspend fun updateShowCompleted(completed: Boolean) {
dataStore.updateData { preferences ->
preferences.toBuilder().setShowCompleted(completed).build()
}
}
À ce stade, l'application doit compiler, mais la fonctionnalité que nous venons de créer dans UserPreferencesRepository
n'est pas utilisée.
8. SharedPreferences vers Proto DataStore
Définir les données à enregistrer dans le fichier .proto
L'ordre de tri est enregistré dans SharedPreferences. Transférons-le vers DataStore. Pour ce faire, commençons par mettre à jour les UserPreferences
dans le fichier .proto pour stocker également l'ordre de tri. L'indicateur SortOrder
étant un enum
, nous devons le définir dans nos UserPreference
. Les enums
sont définis dans des tampons de protocoles, de la même façon qu'avec Kotlin.
Pour les énumérations, la valeur par défaut est la première valeur répertoriée dans la définition de type de l'énumération. Toutefois, lors de la migration à partir de SharedPreferences, nous devons savoir si la valeur que nous avons obtenue est celle par défaut ou bien celle définie précédemment dans SharedPreferences. Pour ce faire, nous définissons une nouvelle valeur dans notre énumération SortOrder
, UNSPECIFIED
, et la répertorions en premier pour qu'elle puisse devenir la valeur par défaut.
Le fichier user_prefs.proto
doit se présenter comme suit :
syntax = "proto3";
option java_package = "com.codelab.android.datastore";
option java_multiple_files = true;
message UserPreferences {
// filter for showing / hiding completed tasks
bool show_completed = 1;
// defines tasks sorting order: no order, by deadline, by priority, by deadline and priority
enum SortOrder {
UNSPECIFIED = 0;
NONE = 1;
BY_DEADLINE = 2;
BY_PRIORITY = 3;
BY_DEADLINE_AND_PRIORITY = 4;
}
// user selected tasks sorting order
SortOrder sort_order = 2;
}
Nettoyez et recréez votre projet pour vous assurer qu'un nouvel objet UserPreferences
a bien été généré et qu'il contient le nouveau champ.
Maintenant que SortOrder
est défini dans le fichier .proto, nous pouvons supprimer la déclaration dans UserPreferencesRepository
. Supprimez ce code :
enum class SortOrder {
NONE,
BY_DEADLINE,
BY_PRIORITY,
BY_DEADLINE_AND_PRIORITY
}
Assurez-vous que la bonne importation SortOrder
est utilisée partout :
import com.codelab.android.datastore.UserPreferences.SortOrder
Dans la fonction TasksViewModel.filterSortTasks()
, nous faisons différentes actions en fonction du type de SortOrder
. Maintenant que nous avons également ajouté l'option UNSPECIFIED
, nous devons ajouter un autre cas pour l'instruction when(sortOrder)
. Étant donné que nous ne souhaitons pas gérer d'autres options au-delà de celles que nous gérons déjà actuellement, nous pouvons simplement générer une UnsupportedOperationException
dans d'autres cas.
Notre fonction filterSortTasks()
ressemble maintenant à ceci :
private fun filterSortTasks(
tasks: List<Task>,
showCompleted: Boolean,
sortOrder: SortOrder
): List<Task> {
// filter the tasks
val filteredTasks = if (showCompleted) {
tasks
} else {
tasks.filter { !it.completed }
}
// sort the tasks
return when (sortOrder) {
SortOrder.UNSPECIFIED -> filteredTasks
SortOrder.NONE -> filteredTasks
SortOrder.BY_DEADLINE -> filteredTasks.sortedByDescending { it.deadline }
SortOrder.BY_PRIORITY -> filteredTasks.sortedBy { it.priority }
SortOrder.BY_DEADLINE_AND_PRIORITY -> filteredTasks.sortedWith(
compareByDescending<Task> { it.deadline }.thenBy { it.priority }
)
// We shouldn't get any other values
else -> throw UnsupportedOperationException("$sortOrder not supported")
}
}
Migrer à partir de SharedPreferences
Pour faciliter la migration, DataStore définit la classe SharedPreferencesMigration
. La méthode by dataStore
qui crée DataStore (utilisée dans TasksActivity
) affiche également un paramètre produceMigrations
. Dans ce bloc, nous créons la liste des DataMigration
qui doivent s'exécuter pour cette instance DataStore. Dans notre cas, nous n'avons qu'une seule migration : SharedPreferencesMigration
.
Lors de l'implémentation de SharedPreferencesMigration
, le bloc migrate
nous donne deux paramètres :
SharedPreferencesView
, qui nous permet de récupérer les données de SharedPreferences- Les données actuelles
UserPreferences
Nous devrons renvoyer un objet UserPreferences
.
Lors de l'intégration du bloc migrate
, nous devrons procéder comme suit :
- Vérifier la valeur de
sortOrder
dansUserPreferences
. - Si la valeur est
SortOrder.UNSPECIFIED
, cela signifie que nous devons récupérer la valeur à partir de SharedPreferences. Si l'élémentSortOrder
est manquant, nous pouvons utiliser la valeurSortOrder.NONE
par défaut. - Une fois l'ordre de tri obtenu, nous devons convertir l'objet
UserPreferences
en compilateur, définir l'ordre de tri, puis recréer l'objet en appelantbuild()
. Aucun autre champ ne sera affecté par cette modification. - Si la valeur
sortOrder
deUserPreferences
n'est pasSortOrder.UNSPECIFIED
, nous pouvons simplement renvoyer les données actuelles figurant dansmigrate
, car la migration a déjà été effectuée.
private val Context.userPreferencesStore: DataStore<UserPreferences> by dataStore(
fileName = DATA_STORE_FILE_NAME,
serializer = UserPreferencesSerializer,
produceMigrations = { context ->
listOf(
SharedPreferencesMigration(
context,
USER_PREFERENCES_NAME
) { sharedPrefs: SharedPreferencesView, currentData: UserPreferences ->
// Define the mapping from SharedPreferences to UserPreferences
if (currentData.sortOrder == SortOrder.UNSPECIFIED) {
currentData.toBuilder().setSortOrder(
SortOrder.valueOf(
sharedPrefs.getString(SORT_ORDER_KEY, SortOrder.NONE.name)!!
)
).build()
} else {
currentData
}
}
)
}
)
Maintenant que nous avons défini la logique de migration, nous devons indiquer à DataStore qu'il doit l'utiliser. Pour ce faire, mettez à jour le compilateur DataStore et attribuez au paramètre migrations
une nouvelle liste contenant une instance de notre SharedPreferencesMigration
:
private val dataStore: DataStore<UserPreferences> = context.createDataStore(
fileName = "user_prefs.pb",
serializer = UserPreferencesSerializer,
migrations = listOf(sharedPrefsMigration)
)
Enregistrer l'ordre de tri dans DataStore
Pour mettre à jour l'ordre de tri lorsque enableSortByDeadline()
et enableSortByPriority()
sont appelés, procédez comme suit :
- Appelez leurs fonctionnalités respectives dans le lambda de
dataStore.updateData()
. updateData()
étant une fonction de suspension, il faut également faire deenableSortByDeadline()
etenableSortByPriority()
des fonctions de suspension.- Utilisez les
UserPreferences
actuelles reçues à partir deupdateData()
pour créer le nouvel ordre de tri. - Mettez à jour les
UserPreferences
en les convertissant en compilateur, en définissant le nouvel ordre de tri, puis en créant à nouveau les préférences.
Voici à quoi ressemble l'implémentation de enableSortByDeadline()
. Vous pourrez effectuer vous-même les modifications de enableSortByPriority()
.
suspend fun enableSortByDeadline(enable: Boolean) {
// updateData handles data transactionally, ensuring that if the sort is updated at the same
// time from another thread, we won't have conflicts
dataStore.updateData { preferences ->
val currentOrder = preferences.sortOrder
val newSortOrder =
if (enable) {
if (currentOrder == SortOrder.BY_PRIORITY) {
SortOrder.BY_DEADLINE_AND_PRIORITY
} else {
SortOrder.BY_DEADLINE
}
} else {
if (currentOrder == SortOrder.BY_DEADLINE_AND_PRIORITY) {
SortOrder.BY_PRIORITY
} else {
SortOrder.NONE
}
}
preferences.toBuilder().setSortOrder(newSortOrder).build()
}
}
Vous pouvez à présent supprimer le paramètre de constructeur context
ainsi que tous les usages de SharedPreferences.
9. Mettre à jour TasksViewModel pour utiliser UserPreferencesRepository
Maintenant que UserPreferencesRepository
stocke à la fois les indicateurs show_completed
et sort_order
dans DataStore, et qu'il affiche Flow<UserPreferences>
, mettons à jour le champ TasksViewModel
pour les utiliser.
Supprimons showCompletedFlow
et sortOrderFlow
, et à la place créons une valeur appelée userPreferencesFlow
qui est initialisée avec userPreferencesRepository.userPreferencesFlow
:
private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow
Lors de la création de tasksUiModelFlow
, remplaçons showCompletedFlow
et sortOrderFlow
par userPreferencesFlow
. Remplaçons les paramètres en conséquence.
Lors de l'appel de filterSortTasks
, transférons showCompleted
et sortOrder
de userPreferences
. Le code doit se présenter comme suit :
private val tasksUiModelFlow = combine(
repository.tasks,
userPreferencesFlow
) { tasks: List<Task>, userPreferences: UserPreferences ->
return@combine TasksUiModel(
tasks = filterSortTasks(
tasks,
userPreferences.showCompleted,
userPreferences.sortOrder
),
showCompleted = userPreferences.showCompleted,
sortOrder = userPreferences.sortOrder
)
}
La fonction showCompletedTasks()
doit à présent être mise à jour pour appeler userPreferencesRepository.updateShowCompleted()
. Comme il s'agit d'une fonction de suspension, créons une coroutine dans viewModelScope
:
fun showCompletedTasks(show: Boolean) {
viewModelScope.launch {
userPreferencesRepository.updateShowCompleted(show)
}
}
Les fonctions userPreferencesRepository
, enableSortByDeadline()
et enableSortByPriority()
sont maintenant des fonctions de suspension. Elles doivent donc aussi être appelées dans une nouvelle coroutine, lancée dans viewModelScope
:
fun enableSortByDeadline(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByDeadline(enable)
}
}
fun enableSortByPriority(enable: Boolean) {
viewModelScope.launch {
userPreferencesRepository.enableSortByPriority(enable)
}
}
Nettoyer UserPreferencesRepository
Supprimez les champs et les méthodes dont vous n'avez plus besoin. Vous devriez pouvoir supprimer les éléments suivants :
_sortOrderFlow
sortOrderFlow
updateSortOrder()
private val sortOrder: SortOrder
private val sharedPreferences
L'application devrait maintenant se compiler correctement. Exécutons-la pour voir si les indicateurs show_completed
et sort_order
sont correctement enregistrés.
Consultez la branche proto_datastore
du dépôt de l'atelier de programmation pour comparer vos modifications.
10. Conclusion
Maintenant que vous êtes passé à Proto DataStore, récapitulons ce que nous avons appris :
- SharedPreferences présente divers inconvénients : une API synchrone qui semble pouvoir être appelée en toute sécurité sur le thread UI, l'absence de mécanisme de signalement d'erreur, l'absence d'API transactionnelle, etc.
- DataStore remplace SharedPreferences et corrige la plupart des défauts de l'API.
- DataStore dispose d'une API totalement asynchrone basée sur des coroutines Kotlin et Kotlin Flow, gère la migration et la corruption des données, et garantit la cohérence des données.