Travailler avec Proto DataStore

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.

fcb2ffa4e6b77f33.gif

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

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.

Au cours de cette étape, vous serez amené à télécharger le code pour l'intégralité de l'atelier de programmation, puis à exécuter 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 :

Télécharger le code source

  1. Décompressez le code, puis ouvrez le projet dans Android Studio version 3.6 ou ultérieure.
  2. Exécutez la configuration d'exécution de l'application sur un appareil ou un émulateur.

b3c0dfdb92dfed77.png

L'application s'exécute et affiche la liste des tâches :

16eb4ceb800bf131.png

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 limite.

Pour simplifier le code que nous devons utiliser, l'application vous permet d'effectuer ces deux actions uniquement :

  • Activer/désactiver l'affichage des tâches exécutées (les tâches sont masquées par défaut)
  • Trier les tâches par ordre de priorité, par date limite ou par date limite 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 un Flow pour représenter un scénario plus réaliste.
  • La classe UserPreferencesRepository : contient SortOrder, défini en tant que enum. L'ordre de tri actuel est enregistré dans SharedPreferences en tant que String, 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 un RecyclerView
  • 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 d'ordre de tri et d'affichage des tâches exécutées, 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 de TasksRepository
  • Un élément MutableStateFlow<Boolean>, contenant le dernier indicateur d'affichage des tâches exécutées, qui n'est conservé que dans la mémoire.
  • Un élément MutableStateFlow<SortOrder>, contenant la dernière valeur SortOrder.

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 d'affichage des tâches exécutées 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 de SortOrder, 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() et enableSortByPriority(). 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 d'affichage des tâches exécutées et d'ordre de tri 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.

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 Flow)

✅ (via Flow)

API synchrone

✅ (mais appel non sécurisé via un thread UI)

Appel sécurisé via un thread UI

❌*

✅ (le travail est déplacé vers Dispatchers.IO en arrière-plan).

✅ (le travail est déplacé vers Dispatchers.IO en arrière-plan).

Peut signaler des erreurs

Protection contre les exceptions d'exécution

❌**

Comporte une API transactionnelle dotée d'une garantie de cohérence forte

Gestion de la migration des données

✅ (depuis SharedPreferences)

✅ (depuis SharedPreferences)

Sécurité du type

✅ avec tampons de protocole

  • 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.

** 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.

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.12"
}

dependencies {
    implementation  "androidx.datastore:datastore-core:1.0.0-alpha04"
    implementation  "com.google.protobuf:protobuf-javalite:3.10.0"
    ...
}

protobuf {
    protoc {
        artifact = "com.google.protobuf:protoc:3.10.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'
                }
            }
        }
    }
}

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 : l'affichage des tâches exécutées et l'ordre de tri, 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 showCompleted.

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 fun readFrom(input: InputStream): UserPreferences {
        try {
            return UserPreferences.parseFrom(input)
        } catch (exception: InvalidProtocolBufferException) {
            throw CorruptionException("Cannot read proto.", exception)
        }
    }

    override fun writeTo(t: UserPreferences, output: OutputStream) = t.writeTo(output)
}

Créer le DataStore

L'indicateur d'affichage des tâches exécutées est conservé dans la mémoire, dans TasksViewModel. Créons un champ privé DataStore<UserPreferences> dans UserPreferencesRepository, basé sur la méthode d'extension Context.createDataStore(). Cette méthode 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)
private val dataStore: DataStore<UserPreferences> =
    context.createDataStore(
        fileName = "user_prefs.pb",
        serializer = UserPreferencesSerializer)

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.

Nous allons maintenant créer une fonction de suspension permettant de mettre à jour la propriété d'affichage des tâches exécutées 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.

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'ordre de tri é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. Créons-la dans UserPreferencesRepository. 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 :

  1. Vérifier la valeur de sortOrder dans UserPreferences.
  2. Si la valeur est SortOrder.UNSPECIFIED, cela signifie que nous devons récupérer la valeur à partir de SharedPreferences. Si l'élément SortOrder est manquant, nous pouvons utiliser la valeur SortOrder.NONE par défaut.
  3. 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 appelant build(). Aucun autre champ ne sera affecté par cette modification.
  4. Si la valeur sortOrder de UserPreferences n'est pas SortOrder.UNSPECIFIED, nous pouvons simplement renvoyer les données actuelles figurant dans migrate, car la migration a déjà été effectuée.
private val sharedPrefsMigration = 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 de enableSortByDeadline() et enableSortByPriority() des fonctions de suspension.
  • Utilisez les UserPreferences actuelles reçues à partir de updateData() 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()
    }
}

Maintenant que le UserPreferencesRepository stocke à la fois les indicateurs d'affichage des tâches exécutées et d'ordre de tri dans DataStore, et qu'il affiche un Flow<UserPreferences>, mettez à jour le champ TasksViewModel pour les utiliser.

Supprimez showCompletedFlow et sortOrderFlow et, à la place, créez une valeur appelée userPreferencesFlow qui est initialisée avec userPreferencesRepository.userPreferencesFlow :

private val userPreferencesFlow = userPreferencesRepository.userPreferencesFlow

Lors de la création de tasksUiModelFlow, remplacez showCompletedFlow et sortOrderFlow par userPreferencesFlow. Remplacez les paramètres en conséquence.

Lors de l'appel de filterSortTasks, transférez showCompleted et sortOrder des userPreferences. Votre 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éez 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 d'affichage des tâches exécutées et d'ordre de tri sont correctement enregistrés.

Consultez la branche "proto" du dépôt de l'atelier de programmation pour comparer vos modifications.

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.