Preferences Datastore

Introduction

Preferences DataStore est une solution de stockage de données qui vous permet de stocker assez facilement de petites quantité de données dans la mémoire permanente. Les donnés sont stockées sous forme de clé-valeur. Les clés permettent ensuite de retrouver les données. DataStore utilise les coroutines Kotlin et Flow pour stocker les données de manière asynchrone. Preferences Datastore est idéal pour de petites quantités de données comme les préférences utilisateur. Pour des volumes de données plus importants, il est recommandé d'utiliser une base de données comme Room.


Mise en place

Pour pouvoir utiliser la librairie du datastore, il faut quelques ajustement dans les fichiers du gradle:

  1. Accéder à la doc officielle pour savoir qu'elle est la version actuelle de la librairie
  2. Dans le fichier libs.versions.toml, ajouter:

  3. [versions]

    pdsversion = "1.1.1"

    [libraries]

    androidx-datastore-preferences = { module = "androidx.datastore:datastore-preferences", version.ref = "pdsversion" }

  4. Dans le fichier build.gradle.kts (Module :app), ajouter:

  5. dependencies {

      implementation (libs.androidx.datastore.preferences)
    }

  6. Synchroniser le gradle

Exemple 1

Dans cet exemple, on va réaliser une implémentation très simple pour tester le datastore

  • Pour commencer, on va tout placer dans le fichier MainActivity. On verra plus tard comment séparer la gestion du datastore dans un fichier dédié.
  • L'application est minimale. Elle est constituée d'un champ de saisie, un bouton Sauvegarder, un bouton Lire et un autre champ de saisie utilisé pour l'affichage
  • Quand on clique sur le bouton Sauvegarder, le texte contenu dans champ de saisie est enregistré dans le datastore avec une clé spécifique. Si on clique sur le bouton Lire, le texte sauvegardé esr récupérée dans le datastore est affiché dans le champ d'affichage. L'opération de lecture utilise la même clé utilisée lors de l'écriture.
  • C'est une implémentation asynchrone, la lecture du datastore est déclenchée par le clic sur le bouton Lire. On verra plus tard comment déclencher la lecture d'une donnée dès que le système détecte son changement
  • Pour s'assurer que la donnée est bien stockée dans une mémoire permanente, fermez l'application, redémarrer la et cliquez sur le bouton Lire

Exemple 1
package package com.example.pdatastore1

import android.content.Context
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            MyApp()
        }
    }
}

val Context.dataStore by preferencesDataStore(name = "settings")
val key = stringPreferencesKey("STR_KEY")

@Composable
fun MyApp() {
    val context = LocalContext.current
    val scope = rememberCoroutineScope()
    var textToSave by remember { mutableStateOf("") }
    var savedText by remember { mutableStateOf("") }

    Column(
        verticalArrangement = Arrangement.spacedBy(20.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        modifier = Modifier
            .fillMaxSize()
            .padding(20.dp)
    ) {
        OutlinedTextField(
            value = textToSave,
            onValueChange = { textToSave = it },
        )

        Button(onClick = {
            scope.launch {
                writeString(context, textToSave)
            }
        }) {
            Text(text = "Sauvegarder")
        }

        Button(onClick = {
            scope.launch {
                savedText = readString(context).first()
            }
        }) {
            Text(text = "Lire")
        }

        OutlinedTextField(
            value = savedText,
            onValueChange = { savedText = it }
        )
    }
}

suspend fun writeString(context: Context, value: String) {
    context.dataStore.edit { preferences ->
        preferences[key] = value
    }
}

fun readString(context: Context): Flow<String> {
    return context.dataStore.data.map { preferences ->
        preferences[key] ?: ""
    }
}

//fun readString(context: Context) = context.dataStore.data.map { it[key] ?: ""  }



Quelques explications sur le code

  • val Context.dataStore by preferencesDataStore(name = "settings")
    On déclare un DataStore nommé "settings" associé au contexte de l'application. Il est déclaré en globale pour être accessible dans les fonctions writeString et readString.
  • val key = stringPreferencesKey("STR_KEY")
    Définit une clé unique pour identifier les données dans DataStore
  • Le composable principal, MyApp(), gère deux champs de texte et deux boutons. Le premier champ de texte permet de saisir les données à sauvegarder (textToSave), l'autre sert à afficher les données sauvegardées (savedText). Le bouton Sauvegarder demande la sauvegarde. Le bouton Lire demande la relecture
  • suspend fun writeString(context: Context, value: String) {
        context.dataStore.edit { preferences ->
            preferences[key] = value
        }
    }
    
    Cette fonction permet de sauvegarder une chaîne dans le datastore. C'est une fonction de type suspend, elle suspend momentanément l'exécution du reste de l'application. Il faut l'appeler dans une coroutine d'où la variable scope déclarée dans MyApp()
    scope.launch {
                    writeString(context, textToSave)
                }

  • fun readString(context: Context): Flow<String> {
        return context.dataStore.data.map { preferences ->
            preferences[key] ?: ""
        }
    }
    Cette fonction permet de lire une chaîne dans le datastore. Elle n'est pas de type suspend, on n'est pas obligé de l'appeler dans une coroutine, nous l'avons tout de même fait, car cette fonction retourne un flux de données (Flow<String>), et la méthode .first() qui permet de récupérer le premier élément du flux doit être exécutée dans une coroutine
    scope.launch {
                    savedText = readString(context).first()
                }

    On peut utiliser une syntaxe différente pour la fonction de lecture:
    fun readString(context: Context) = context.dataStore.data.map { it[key] ?: ""  }

Exemple 2

Nous allons reprendre le même exemple que précédemment, mais cette fois on va placer la gestion du datastore dans un fichier à part. C'est comme ça que l'on procédera dorénavant.

  1. Ajouter les dépendances dans le gradle comme indiqué dans le paragraphe Mise en place
  2. Ajouter un package au projet. La première lettre du nom du package doit être en minuscule. On peut par exemple l'appeler data
  3. ClicDroit(app/kotlin+java/com.example.nomappli) → new → pakage → data → Enter
  4. Dans ce package, ajouter une dataclasse que l'on peut appeler PDSmanager
  5. Clicdroit(data) → new → Kotlin class/file → PDSmanager → clic(Data class) → Enter
    Le fichier PDSmanager.kt s'ouvre. C'est là que l'on va créer le datastore et les fonctions associées
    Exemple 2: le Datastore manager
    package com.example.pdatastore2.data
    
    import android.content.Context
    import androidx.datastore.preferences.core.edit
    import androidx.datastore.preferences.core.stringPreferencesKey
    import androidx.datastore.preferences.preferencesDataStore
    import kotlinx.coroutines.flow.Flow
    import kotlinx.coroutines.flow.map
    
    private val Context.dataStore by preferencesDataStore("PDSEX2")
    
    data class PDSmanager(private val context: Context){
    
        suspend fun writeString(strkey: String, value: String) {
            val key = stringPreferencesKey(strkey)
            context.dataStore.edit { preferences ->
                preferences[key] = value
            }
        }
    
        fun readString(strkey: String,): Flow<String> {
            val key = stringPreferencesKey(strkey)
            return context.dataStore.data.map { preferences ->
                preferences[key] ?: ""
            }
        }
    }
    

  6. Adapter l'UI comme suit:
  7. Exemple 2: MainActivity
    package com.example.pdatastore2
    
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.padding
    import androidx.compose.material3.Button
    import androidx.compose.material3.OutlinedTextField
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.unit.dp
    import com.example.pdatastore2.data.PDSmanager
    import kotlinx.coroutines.flow.first
    import kotlinx.coroutines.launch
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp()
            }
        }
    }
    
    
    @Composable
    fun MyApp() {
        val myPDSmanager = PDSmanager(LocalContext.current)
        val coroutineScope = rememberCoroutineScope()
        var textToSave by remember { mutableStateOf("") }
        var savedText by remember { mutableStateOf("") }
    
        Column(
            verticalArrangement = Arrangement.spacedBy(20.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier
                .fillMaxSize()
                .padding(20.dp)
        ) {
            OutlinedTextField(
                value = textToSave,
                onValueChange = { textToSave = it },
            )
    
            Button(onClick = {
                coroutineScope.launch {
                    myPDSmanager.writeString("KEY2", textToSave)
                }
            }) {
                Text(text = "Sauvegarder")
            }
    
            Button(onClick = {
                coroutineScope.launch {
                    savedText = myPDSmanager.readString("KEY2").first()
                }
            }) {
                Text(text = "Lire")
            }
    
            OutlinedTextField(
                value = savedText,
                onValueChange = { savedText = it },
            )
        }
    }
    
    


Exemple 3

On va reprendre l'exemple précédent. On va supprimer le bouton Lire. On va faire de sorte que la donnée sauvegardée soit actualisée automatiquement dans l'application dès son changement dans le datastore

  • Le PDSmanager ne change pas
  • Adapter l'UI comme suit:
  • Exemple 3: MainActivity
    package com.example.pdatastore3
    
    import android.os.Bundle
    import androidx.activity.ComponentActivity
    import androidx.activity.compose.setContent
    import androidx.compose.foundation.border
    import androidx.compose.foundation.layout.Arrangement
    import androidx.compose.foundation.layout.Column
    import androidx.compose.foundation.layout.fillMaxSize
    import androidx.compose.foundation.layout.padding
    import androidx.compose.foundation.layout.size
    import androidx.compose.foundation.layout.wrapContentHeight
    import androidx.compose.foundation.shape.RoundedCornerShape
    import androidx.compose.material3.Button
    import androidx.compose.material3.OutlinedTextField
    import androidx.compose.material3.Text
    import androidx.compose.runtime.Composable
    import androidx.compose.runtime.collectAsState
    import androidx.compose.runtime.getValue
    import androidx.compose.runtime.mutableStateOf
    import androidx.compose.runtime.remember
    import androidx.compose.runtime.rememberCoroutineScope
    import androidx.compose.runtime.setValue
    import androidx.compose.ui.Alignment
    import androidx.compose.ui.Modifier
    import androidx.compose.ui.graphics.Color
    import androidx.compose.ui.platform.LocalContext
    import androidx.compose.ui.unit.dp
    import com.example.pdatastore3.data.PDSmanager
    import kotlinx.coroutines.launch
    
    class MainActivity : ComponentActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContent {
                MyApp()
            }
        }
    }
    
    
    @Composable
    fun MyApp() {
        val myPDSmanager = PDSmanager(LocalContext.current)
        val coroutineScope = rememberCoroutineScope()
        var textToSave by remember { mutableStateOf("") }
        val savedText by myPDSmanager.readString("KEY3").collectAsState(initial = "")
        
        Column(
            verticalArrangement = Arrangement.spacedBy(20.dp),
            horizontalAlignment = Alignment.CenterHorizontally,
            modifier = Modifier
                .fillMaxSize()
                .padding(20.dp)
        ) {
            OutlinedTextField(
                value = textToSave,
                onValueChange = { textToSave = it },
            )
    
            Button(onClick = {
                coroutineScope.launch {
                    myPDSmanager.writeString("KEY3", textToSave)
                }
            }) {
                Text(text = "Sauvegarder")
            }
            
            Text(text =savedText,
                modifier = Modifier
                    .size(300.dp, 50.dp)
                    .border( width=2.dp, Color.Blue, shape = RoundedCornerShape(5.dp) )
                    .wrapContentHeight(Alignment.CenterVertically)
                    .padding(start = 10.dp)
            )
        }
    }