Stocker les préférences localement avec DataStore

1. Avant de commencer

Introduction

Dans ce module, vous avez appris à enregistrer des données localement sur un appareil à l'aide de SQL et de Room. SQL et Room sont des outils puissants. Toutefois, si vous n'avez pas besoin de stocker de données relationnelles, DataStore peut fournir une solution simple. Le composant Jetpack DataStore est un excellent moyen de stocker de petits ensembles de données simples en ne consommant que peu de ressources. DataStore propose deux implémentations différentes : Preferences DataStore et Proto DataStore.

  • Preferences DataStore stocke les paires clé-valeur. Les valeurs peuvent appartenir aux types de données de base de Kotlin, tels que String, Boolean et Integer. Cette implémentation ne stocke pas d'ensembles de données complexes. Elle ne nécessite pas de schéma prédéfini. Le principal cas d'utilisation de Preferences Datastore consiste à stocker les préférences sur l'appareil de l'utilisateur.
  • Proto DataStore stocke des types de données personnalisés. Cette implémentation nécessite un schéma prédéfini, qui mappe des définitions de protocole avec des structures d'objets.

Cet atelier de programmation ne traite que de Preferences DataStore. Pour en savoir plus sur Proto DataStore, consultez la documentation dédiée à DataStore.

Preferences DataStore est un excellent moyen de stocker des paramètres contrôlés par l'utilisateur. Dans cet atelier de programmation, vous allez apprendre à implémenter DataStore à cette fin.

Conditions préalables :

Ce dont vous avez besoin

  • Un ordinateur avec un accès à Internet et Android Studio
  • Un appareil ou un émulateur
  • Le code de démarrage pour l'application Dessert Release

Objectifs de l'atelier

L'application Dessert Release affiche la liste des versions Android. L'icône de la barre d'application permet de passer du mode Grille au mode Liste.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

Dans son état actuel, l'application ne conserve pas la mise en page sélectionnée. Lorsque vous fermez l'application, votre sélection n'est pas enregistrée et la mise en page par défaut est rétablie. Dans cet atelier de programmation, vous allez ajouter DataStore à l'application Dessert Release et l'utiliser pour stocker la préférence de mise en page sélectionnée.

2. Télécharger le code de démarrage

Cliquez sur le lien ci-dessous pour télécharger l'ensemble du code de cet atelier de programmation :

Ou, si vous préférez, vous pouvez cloner le code de la version Dessert à partir de GitHub :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout starter
  1. Dans Android Studio, ouvrez le dossier basic-android-kotlin-compose-training-dessert-release.
  2. Ouvrez le code de l'application Dessert Release dans Android Studio.

3. Configurer des dépendances

Ajoutez ce qui suit à dependencies dans le fichier app/build.gradle.kts :

implementation("androidx.datastore:datastore-preferences:1.0.0")

4. Implémenter le dépôt de préférences utilisateur

  1. Dans le package data, créez une classe intitulée UserPreferencesRepository.

c4c2e90902898001.png

  1. Dans le constructeur UserPreferencesRepository, définissez une propriété de valeur privée pour représenter une instance d'objet DataStore de type Preferences.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
}

DataStore stocke les paires clé-valeur. Pour accéder à une valeur, vous devez définir une clé.

  1. Créez un companion object dans la classe UserPreferencesRepository.
  2. Utilisez la fonction booleanPreferencesKey() pour définir une clé et lui transmettre le nom is_linear_layout. Comme pour les noms de tables SQL, les espaces dans le nom de la clé doivent être remplacés par des traits de soulignement. Cette clé permet d'accéder à une valeur booléenne indiquant si la mise en page linéaire doit être affichée.
class UserPreferencesRepository(
    private val dataStore: DataStore<Preferences>
){
    private companion object {
        val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    }
    ...
}

Écrire dans le DataStore

Vous créez et modifiez les valeurs dans un DataStore en transmettant un lambda à la méthode edit(). Le lambda reçoit une instance de MutablePreferences, que vous pouvez utiliser pour mettre à jour les valeurs dans DataStore. Toutes les mises à jour de ce lambda sont exécutées comme une transaction unique. En d'autres termes, la mise à jour est atomique. Tout est géré simultanément. Ce type de mise à jour permet d'éviter que certaines valeurs se mettent à jour, mais pas d'autres.

  1. Créez une fonction de suspension et appelez-la saveLayoutPreference().
  2. Dans la fonction saveLayoutPreference(), appelez la méthode edit() sur l'objet dataStore.
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit {

    }
}
  1. Pour rendre votre code plus lisible, définissez un nom pour l'élément MutablePreferences fourni dans le corps du lambda. Utilisez cette propriété pour déterminer une valeur avec la clé que vous avez définie et la valeur booléenne transmise à la fonction saveLayoutPreference().
suspend fun saveLayoutPreference(isLinearLayout: Boolean) {
    dataStore.edit { preferences ->
        preferences[IS_LINEAR_LAYOUT] = isLinearLayout
    }
}

Lire depuis le DataStore

Maintenant que vous avez créé un moyen d'écrire isLinearLayout dans dataStore, procédez comme suit pour le lire :

  1. Dans UserPreferencesRepository, créez une propriété de type Flow<Boolean> appelée isLinearLayout.
val isLinearLayout: Flow<Boolean> =
  1. Vous pouvez utiliser la propriété DataStore.data pour exposer les valeurs DataStore. Définissez isLinearLayout sur la propriété data de l'objet DataStore.
val isLinearLayout: Flow<Boolean> = dataStore.data

La propriété data est un Flow d'objets Preferences. L'objet Preferences contient toutes les paires clé-valeur du DataStore. Chaque fois que les données dans le DataStore sont mises à jour, un nouvel objet Preferences est émis dans Flow.

  1. Utilisez la fonction Map pour convertir Flow<Preferences> en Flow<Boolean>.

Cette fonction accepte un lambda avec l'objet Preferences actuel comme paramètre. Vous pouvez spécifier la clé que vous avez définie précédemment pour obtenir la préférence de mise en page. Gardez à l'esprit qu'il se peut que la valeur n'existe pas si saveLayoutPreference n'a pas encore été appelé. Vous devez donc également fournir une valeur par défaut.

  1. Spécifiez true pour utiliser par défaut la mise en page en lignes.
val isLinearLayout: Flow<Boolean> = dataStore.data.map { preferences ->
    preferences[IS_LINEAR_LAYOUT] ?: true
}

Gestion des exceptions

Chaque interaction avec le système de fichiers d'un appareil peut donner lieu à un dysfonctionnement. Par exemple, il se peut qu'un fichier n'existe pas, que le disque soit saturé ou qu'il ait été retiré. DataStore lit et écrit des données à partir de fichiers. Des IOExceptions peuvent survenir lorsque vous accédez à DataStore. Utilisez l'opérateur catch{} pour intercepter les exceptions et gérer ces échecs.

  1. Dans l'objet associé, implémentez une propriété de chaîne TAG immuable pour la journalisation.
private companion object {
    val IS_LINEAR_LAYOUT = booleanPreferencesKey("is_linear_layout")
    const val TAG = "UserPreferencesRepo"
}
  1. Preferences DataStore génère une IOException si une erreur se produit lors de la lecture des données. Dans le bloc d'initialisation isLinearLayout, avant map(), utilisez l'opérateur catch{} pour intercepter l'IOException.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {}
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }
  1. Dans le bloc d'interception, si une IOexception survient, consignez l'erreur et émettez emptyPreferences(). Si une exception d'un autre type survient, il est préférable de la renvoyer. En émettant emptyPreferences() en cas d'erreur, la fonction Map peut toujours être mappée sur la valeur par défaut.
val isLinearLayout: Flow<Boolean> = dataStore.data
    .catch {
        if(it is IOException) {
            Log.e(TAG, "Error reading preferences.", it)
            emit(emptyPreferences())
        } else {
            throw it
        }
    }
    .map { preferences ->
        preferences[IS_LINEAR_LAYOUT] ?: true
    }

5. Initialiser le DataStore

Dans cet atelier de programmation, vous devez gérer manuellement l'injection de dépendances. Par conséquent, vous devez fournir manuellement une classe UserPreferencesRepository avec Preferences DataStore. Procédez comme suit pour injecter DataStore dans UserPreferencesRepository.

  1. Recherchez le package dessertrelease.
  2. Dans ce répertoire, créez une classe appelée DessertReleaseApplication et implémentez la classe Application. Il s'agit du conteneur de votre DataStore.
class DessertReleaseApplication: Application() {
}
  1. Dans le fichier DessertReleaseApplication.kt, mais en dehors de la classe DessertReleaseApplication, déclarez un private const val appelé LAYOUT_PREFERENCE_NAME.
  2. Attribuez à la variable LAYOUT_PREFERENCE_NAME la valeur de chaîne layout_preferences, que vous pourrez ensuite utiliser comme nom du Preferences Datastore qui sera instancié à l'étape suivante.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
  1. Toujours en dehors du corps de la classe DessertReleaseApplication, mais dans le fichier DessertReleaseApplication.kt, créez une propriété de valeur privée de type DataStore<Preferences>, appelée Context.dataStore, à l'aide du délégué preferencesDataStore. Transmettez LAYOUT_PREFERENCE_NAME pour le paramètre name du délégué preferencesDataStore.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)
  1. Dans le corps de la classe DessertReleaseApplication, créez une instance lateinit var du UserPreferencesRepository.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository
}
  1. Remplacez la méthode onCreate().
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
    }
}
  1. Dans la méthode onCreate(), initialisez userPreferencesRepository en construisant un UserPreferencesRepository avec dataStore comme paramètre.
private const val LAYOUT_PREFERENCE_NAME = "layout_preferences"
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = LAYOUT_PREFERENCE_NAME
)

class DessertReleaseApplication: Application() {
    lateinit var userPreferencesRepository: UserPreferencesRepository

    override fun onCreate() {
        super.onCreate()
        userPreferencesRepository = UserPreferencesRepository(dataStore)
    }
}
  1. Ajoutez la ligne suivante dans la balise <application> du fichier AndroidManifest.xml.
<application
    android:name=".DessertReleaseApplication"
    ...
</application>

Cette approche définit la classe DessertReleaseApplication comme point d'entrée de l'application. Ce code vise à initialiser les dépendances définies dans la classe DessertReleaseApplication avant de lancer MainActivity.

6. Utiliser UserPreferencesRepository

Fournir le dépôt au ViewModel

Maintenant que UserPreferencesRepository est disponible par injection de dépendances, vous pouvez l'utiliser dans DessertReleaseViewModel.

  1. Dans DessertReleaseViewModel, créez une propriété UserPreferencesRepository en tant que paramètre constructeur.
class DessertReleaseViewModel(
    private val userPreferencesRepository: UserPreferencesRepository
) : ViewModel() {
    ...
}
  1. Dans l'objet associé à ViewModel, dans le bloc viewModelFactory initializer, obtenez une instance de DessertReleaseApplication à l'aide du code suivant.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                ...
            }
        }
    }
}
  1. Créez une instance de DessertReleaseViewModel et transmettez userPreferencesRepository.
    ...
    companion object {
        val Factory: ViewModelProvider.Factory = viewModelFactory {
            initializer {
                val application = (this[APPLICATION_KEY] as DessertReleaseApplication)
                DessertReleaseViewModel(application.userPreferencesRepository)
            }
        }
    }
}

La classe UserPreferencesRepository est désormais accessible par le ViewModel. Les étapes suivantes consistent à utiliser les fonctionnalités de lecture et d'écriture de UserPreferencesRepository, que vous avez implémentées précédemment.

Enregistrer la préférence de mise en page

  1. Modifiez la fonction selectLayout() dans DessertReleaseViewModel pour accéder au dépôt de préférences et mettre à jour la préférence de mise en page.
  2. N'oubliez pas que l'écriture dans DataStore est effectuée de manière asynchrone avec une fonction suspend. Démarrez une nouvelle coroutine pour appeler la fonction saveLayoutPreference() du dépôt de préférences.
fun selectLayout(isLinearLayout: Boolean) {
    viewModelScope.launch {
        userPreferencesRepository.saveLayoutPreference(isLinearLayout)
    }
}

Lire la préférence de mise en page

Dans cette section, vous allez refactoriser le uiState: StateFlow existant dans le ViewModel pour refléter le isLinearLayout: Flow du dépôt.

  1. Supprimez le code qui initialise la propriété uiState sur MutableStateFlow(DessertReleaseUiState).
val uiState: StateFlow<DessertReleaseUiState> =

Dans le dépôt, la préférence pour une mise en page en lignes peut être "true" ou "false", et est indiquée sous forme de Flow<Boolean>. Cette valeur doit être mappée à un état de l'interface utilisateur.

  1. Définissez StateFlow sur le résultat de la transformation de collection map() appelée sur isLinearLayout Flow.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
}
  1. Renvoyez une instance de la classe de données DessertReleaseUiState en transmettant isLinearLayout Boolean. L'écran utilise cet état d'interface utilisateur pour déterminer les chaînes et les icônes à afficher.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }

UserPreferencesRepository.isLinearLayout est un Flow froid. Toutefois, pour fournir l'état à l'interface utilisateur, il est préférable d'utiliser un flux chaud tel que StateFlow, afin que l'état soit toujours disponible immédiatement.

  1. Utilisez la fonction stateIn() pour convertir un Flow en StateFlow.
  2. La fonction stateIn() accepte trois paramètres : scope, started et initialValue. Transmettez respectivement viewModelScope, SharingStarted.WhileSubscribed(5_000) et DessertReleaseUiState() pour ces paramètres.
val uiState: StateFlow<DessertReleaseUiState> =
    userPreferencesRepository.isLinearLayout.map { isLinearLayout ->
        DessertReleaseUiState(isLinearLayout)
    }
.stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5_000),
        initialValue = DessertReleaseUiState()
    )
  1. Lancez l'application. Notez que vous pouvez cliquer sur l'icône de mise en page pour passer du mode Grille au mode Liste.

b6e4bd0e50915b81.png 24a261db4cf2c6b8.png

Félicitations ! Vous avez bien ajouté Preferences DataStore à votre application pour enregistrer les préférences de mise en page de l'utilisateur.

7. Télécharger le code de solution

Pour télécharger le code de cet atelier de programmation terminé, utilisez les commandes Git suivantes :

$ git clone https://github.com/google-developer-training/basic-android-kotlin-compose-training-dessert-release.git
$ cd basic-android-kotlin-compose-training-dessert-release
$ git checkout main

Vous pouvez également télécharger le dépôt sous forme de fichier ZIP, le décompresser et l'ouvrir dans Android Studio.

Si vous souhaitez voir le code de solution, affichez-le sur GitHub.