DataStore   Fait partie d'Android Jetpack.

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 utilise les coroutines Kotlin et Flow pour stocker les données de manière asynchrone, cohérente et transactionnelle.

Si vous utilisez actuellement SharedPreferences pour stocker des données, nous vous recommandons d'effectuer une migration vers DataStore.

Preferences DataStore et Proto DataStore

DataStore propose deux implémentations différentes : Preferences DataStore et Proto DataStore.

  • Preferences DataStore stocke des données et y accède à l'aide de clés. Cette implémentation ne nécessite pas de schéma prédéfini et ne garantit pas la sécurité du typage.
  • Proto DataStore stocke les données comme instances d'un type de données personnalisé. Cette implémentation nécessite que vous définissiez un schéma à l'aide de tampons de protocole et vous permet de bénéficier de la sécurité du typage.

Utiliser correctement DataStore

Pour utiliser correctement DataStore, gardez toujours à l'esprit les règles suivantes :

  1. Ne créez jamais plus d'une instance de DataStore pour un fichier donné dans le même processus. En effet, les fonctionnalités de DataStore pourraient ne plus marcher. Si plusieurs instances sont actives pour un fichier donné au cours du même processus, DataStore générera une IllegalStateException lors de la lecture ou de la mise à jour de données.

  2. Le type générique de DataStore doit être immuable. La mutation d'un type utilisé dans DataStore annule toute garantie fournie par DataStore et crée des bugs potentiellement graves et difficiles à détecter. Nous vous recommandons vivement d'utiliser des tampons de protocole, qui offrent des garanties d'immuabilité, une API simple et une sérialisation efficace.

  3. N'utilisez jamais SingleProcessDataStore et MultiProcessDataStore pour le même fichier. Si vous avez l'intention d'accéder à DataStore depuis plusieurs processus, utilisez toujours MultiProcessDataStore.

Configuration

Pour utiliser Jetpack DataStore dans votre application, ajoutez les éléments suivants à votre fichier Gradle en fonction de l'implémentation que vous souhaitez utiliser :

Preferences DataStore

    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation "androidx.datastore:datastore-preferences:1.1.2"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-preferences-rxjava2:1.1.2"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-preferences-rxjava3:1.1.2"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-preferences-core:1.1.2"
    }
    
    // Preferences DataStore (SharedPreferences like APIs)
    dependencies {
        implementation("androidx.datastore:datastore-preferences:1.1.2")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-preferences-rxjava2:1.1.2")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-preferences-rxjava3:1.1.2")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-preferences-core:1.1.2")
    }
    

Proto DataStore

    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation "androidx.datastore:datastore:1.1.2"

        // optional - RxJava2 support
        implementation "androidx.datastore:datastore-rxjava2:1.1.2"

        // optional - RxJava3 support
        implementation "androidx.datastore:datastore-rxjava3:1.1.2"
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation "androidx.datastore:datastore-core:1.1.2"
    }
    
    // Typed DataStore (Typed API surface, such as Proto)
    dependencies {
        implementation("androidx.datastore:datastore:1.1.2")

        // optional - RxJava2 support
        implementation("androidx.datastore:datastore-rxjava2:1.1.2")

        // optional - RxJava3 support
        implementation("androidx.datastore:datastore-rxjava3:1.1.2")
    }

    // Alternatively - use the following artifact without an Android dependency.
    dependencies {
        implementation("androidx.datastore:datastore-core:1.1.2")
    }
    

Stocker des paires clé-valeur avec Preferences DataStore

L'implémentation Preferences DataStore utilise les classes DataStore et Preferences pour conserver des paires clé-valeur simples sur le disque.

Créer un Preferences DataStore

Utilisez le délégué de propriété créé par preferencesDataStore pour créer une instance de DataStore<Preferences>. Appelez-le une fois au niveau supérieur de votre fichier Kotlin et accédez-y via cette propriété dans le reste de votre application. Il est ainsi plus facile de conserver votre DataStore en tant que singleton. Si vous utilisez RxJava, vous pouvez également utiliser RxPreferenceDataStoreBuilder. Le paramètre name obligatoire est le nom du Preferences DataStore.

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
RxDataStore<Preferences> dataStore =
 
new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

Lire depuis un Preferences DataStore

Étant donné que Preferences DataStore n'utilise pas de schéma prédéfini, vous devez utiliser la fonction de type de clé correspondante pour définir une clé pour chaque valeur à stocker dans l'instance DataStore<Preferences>. Par exemple, pour définir une clé pour une valeur int, utilisez intPreferencesKey(). Utilisez ensuite la propriété DataStore.data pour exposer la valeur stockée appropriée à l'aide d'un Flow.

val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
val exampleCounterFlow: Flow<Int> = context.dataStore.data
 
.map { preferences ->
   
// No type safety.
    preferences
[EXAMPLE_COUNTER] ?: 0
}
Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

Flowable<Integer> exampleCounterFlow =
  dataStore
.data().map(prefs -> prefs.get(EXAMPLE_COUNTER));

Écrire dans un Preferences DataStore

Preferences DataStore fournit une fonction edit() qui met à jour les données dans DataStore de manière transactionnelle. Le paramètre transform 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.

suspend fun incrementCounter() {
  context
.dataStore.edit { settings ->
   
val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings
[EXAMPLE_COUNTER] = currentCounterValue + 1
 
}
}
Single<Preferences> updateResult =  dataStore.updateDataAsync(prefsIn -> {
 
MutablePreferences mutablePreferences = prefsIn.toMutablePreferences();
 
Integer currentInt = prefsIn.get(INTEGER_KEY);
  mutablePreferences
.set(INTEGER_KEY, currentInt != null ? currentInt + 1 : 1);
 
return Single.just(mutablePreferences);
});
// The update is completed once updateResult is completed.

Stocker des objets typés avec Proto DataStore

L'implémentation Proto DataStore utilise DataStore et des tampons de protocole pour conserver des objets typés sur le disque.

Définir un schéma

Proto DataStore nécessite un schéma prédéfini dans un fichier .proto du répertoire app/src/main/proto/. Ce schéma définit le type des objets que vous conservez dans votre Proto DataStore. Pour en savoir plus sur la définition d'un schéma .proto, consultez le Guide du langage des tampons de protocole.

syntax = "proto3";

option java_package = "com.example.application";
option java_multiple_files = true;

message Settings {
  int32 example_counter = 1;
}

Créer un Proto DataStore

La création d'un Proto DataStore pour stocker vos objets typés s'effectue en deux étapes :

  1. Définissez une classe qui implémente Serializer<T>, où T est le type défini dans le fichier .proto. Cette classe de sérialiseur indique à DataStore comment lire et écrire votre type de données. Assurez-vous d'inclure une valeur par défaut à utiliser pour le sérialiseur si aucun fichier n'a encore été créé.
  2. Utilisez le délégué de propriété créé par dataStore pour créer une instance de DataStore<T>, où T est le type défini dans le fichier .proto. Appelez-le une fois au niveau supérieur de votre fichier Kotlin et accédez-y via ce délégué dans le reste de votre application. Le paramètre filename indique à DataStore le fichier à utiliser pour stocker les données, et le paramètre serializer indique à DataStore le nom de la classe de sérialiseur définie à l'étape 1.
object SettingsSerializer : Serializer<Settings> {
 
override val defaultValue: Settings = Settings.getDefaultInstance()

 
override suspend fun readFrom(input: InputStream): Settings {
   
try {
     
return Settings.parseFrom(input)
   
} catch (exception: InvalidProtocolBufferException) {
     
throw CorruptionException("Cannot read proto.", exception)
   
}
 
}

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

val Context.settingsDataStore: DataStore<Settings> by dataStore(
  fileName
= "settings.pb",
  serializer
= SettingsSerializer
)
private static class SettingsSerializer implements Serializer<Settings> {
 
@Override
 
public Settings getDefaultValue() {
   
Settings.getDefaultInstance();
 
}

 
@Override
 
public Settings readFrom(@NotNull InputStream input) {
   
try {
     
return Settings.parseFrom(input);
   
} catch (exception: InvalidProtocolBufferException) {
     
throw CorruptionException(“Cannot read proto.”, exception);
   
}
 
}

 
@Override
 
public void writeTo(Settings t, @NotNull OutputStream output) {
    t
.writeTo(output);
 
}
}

RxDataStore<Byte> dataStore =
   
new RxDataStoreBuilder<Byte>(context, /* fileName= */ "settings.pb", new SettingsSerializer()).build();

Lire à partir d'un Proto DataStore

Utilisez DataStore.data pour exposer un Flow de la propriété appropriée à partir de votre objet stocké.

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
 
.map { settings ->
   
// The exampleCounter property is generated from the proto schema.
    settings
.exampleCounter
 
}
Flowable<Integer> exampleCounterFlow =
  dataStore
.data().map(settings -> settings.getExampleCounter());

Écrire dans un Proto DataStore

Proto DataStore fournit une fonction updateData() qui met à jour un objet stocké de manière transactionnelle. updateData() vous donne l'état actuel des données comme instance de votre type de données, et met à jour les données de manière transactionnelle en une opération atomique de lecture-écriture-modification.

suspend fun incrementCounter() {
  context
.settingsDataStore.updateData { currentSettings ->
    currentSettings
.toBuilder()
     
.setExampleCounter(currentSettings.exampleCounter + 1)
     
.build()
   
}
}
Single<Settings> updateResult =
  dataStore
.updateDataAsync(currentSettings ->
   
Single.just(
      currentSettings
.toBuilder()
       
.setExampleCounter(currentSettings.getExampleCounter() + 1)
       
.build()));

Utiliser DataStore en code synchrone

L'un des principaux avantages de DataStore est l'API asynchrone, mais il n'est pas toujours possible de modifier le code environnant pour qu'il soit asynchrone. Cela peut être le cas si vous travaillez avec un codebase existant qui utilise des E/S de disque synchrones ou si vous avez une dépendance qui ne fournit pas d'API asynchrone.

Les coroutines Kotlin fournissent le constructeur de coroutines runBlocking() pour aider à combler l'écart entre le code synchrone et asynchrone. Vous pouvez utiliser runBlocking() pour lire des données de DataStore de manière synchrone. RxJava propose des méthodes de blocage sur Flowable. Le code suivant bloque le thread d'appel jusqu'à ce que DataStore renvoie des données :

val exampleData = runBlocking { context.dataStore.data.first() }
Settings settings = dataStore.data().blockingFirst();

Effectuer des opérations d'E/S synchrones sur le thread UI peut entraîner des erreurs ANR ou des à-coups dans l'interface utilisateur. Vous pouvez limiter ces problèmes en préchargeant les données de DataStore de manière asynchrone :

override fun onCreate(savedInstanceState: Bundle?) {
    lifecycleScope
.launch {
        context
.dataStore.data.first()
       
// You should also handle IOExceptions here.
   
}
}
dataStore.data().first().subscribe();

De cette façon, DataStore lit les données de manière asynchrone et les met en cache. Les lectures synchrones ultérieures utilisant runBlocking() peuvent être plus rapides ou éviter complètement une opération d'E/S sur le disque si la lecture initiale est terminée.

Utiliser DataStore dans un code multiprocessus

Vous pouvez configurer DataStore pour accéder aux mêmes données dans différents processus avec les mêmes garanties de cohérence des données que dans un seul processus. En particulier, DataStore garantit que :

  • les lectures ne renvoient que les données qui ont été conservées sur le disque ;
  • la lecture après écriture est cohérente ;
  • les écritures sont sérialisées ;
  • les lectures ne sont jamais bloquées par les écritures.

Prenons l'exemple d'une application avec un service et une activité :

  1. Le service s'exécute dans un processus distinct et met régulièrement à jour le DataStore :

    <service
     
    android:name=".MyService"
     
    android:process=":my_process_id" />
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
          scope
    .launch {
             
    while(isActive) {
                  dataStore
    .updateData {
                     
    Settings(lastUpdate = System.currentTimeMillis())
                 
    }
                  delay
    (1000)
             
    }
         
    }
    }
  2. Pendant que l'application collecte ces modifications et met à jour son interface utilisateur :

    val settings: Settings by dataStore.data.collectAsState()
    Text(
      text
    = "Last updated: $${settings.timestamp}",
    )

Pour utiliser DataStore dans différents processus, vous devez construire l'objet DataStore à l'aide de MultiProcessDataStoreFactory.

val dataStore: DataStore<Settings> = MultiProcessDataStoreFactory.create(
   serializer
= SettingsSerializer(),
   produceFile
= {
       
File("${context.cacheDir.path}/myapp.preferences_pb")
   
}
)

serializer indique à DataStore comment lire et écrire votre type de données. Assurez-vous d'inclure une valeur par défaut à utiliser pour le sérialiseur si aucun fichier n'a encore été créé. Voici un exemple d'implémentation utilisant kotlinx.serialization :

@Serializable
data class Settings(
   
val lastUpdate: Long
)

@Singleton
class SettingsSerializer @Inject constructor() : Serializer<Settings> {

   
override val defaultValue = Settings(lastUpdate = 0)

   
override suspend fun readFrom(input: InputStream): Timer =
       
try {
           
Json.decodeFromString(
               
Settings.serializer(), input.readBytes().decodeToString()
           
)
       
} catch (serialization: SerializationException) {
           
throw CorruptionException("Unable to read Settings", serialization)
       
}

   
override suspend fun writeTo(t: Settings, output: OutputStream) {
       output
.write(
           
Json.encodeToString(Settings.serializer(), t)
               
.encodeToByteArray()
       
)
   
}
}

Vous pouvez utiliser l'injection de dépendances Hilt pour vous assurer que votre instance DataStore est unique pour chaque processus :

@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
   
MultiProcessDataStoreFactory.create(...)

Gérer la corruption de fichiers

Dans de rares cas, le fichier persistant sur disque de DataStore peut être corrompu. Par défaut, DataStore ne récupère pas automatiquement en cas de corruption, et les tentatives de lecture entraînent l'émission d'une exception CorruptionException par le système.

DataStore propose une API de gestion des corruptions qui peut vous aider à récupérer correctement dans un tel scénario et à éviter de générer l'exception. Lorsqu'il est configuré, le gestionnaire de corruption remplace le fichier corrompu par un nouveau contenant une valeur par défaut prédéfinie.

Pour configurer ce gestionnaire, fournissez un corruptionHandler lors de la création de l'instance DataStore dans by dataStore() ou dans la méthode de fabrique DataStoreFactory:

val dataStore: DataStore<Settings> = DataStoreFactory.create(
   serializer
= SettingsSerializer(),
   produceFile
= {
       
File("${context.cacheDir.path}/myapp.preferences_pb")
   
},
   corruptionHandler
= ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)

Envoyer des commentaires

Faites-nous part de vos commentaires et de vos idées via les ressources suivantes :

Issue Tracker
Signalez les problèmes pour que nous puissions corriger les bugs.

Ressources supplémentaires

Pour en savoir plus sur Jetpack DataStore, consultez les ressources supplémentaires suivantes :

Exemples

Aucun résultat.

Blogs

Ateliers de programmation

Aucune recommandation pour l'instant.

à votre compte Google.