DataStore   Teil von Android Jetpack.

Jetpack DataStore ist eine Datenspeicherlösung, mit der Sie Schlüssel/Wert-Paare oder typisierte Objekte mit Protokoll-Buffers speichern können. DataStore verwendet Kotlin-Coroutinen und Flow, um Daten asynchron, konsistent und transaktional zu speichern.

Wenn Sie derzeit SharedPreferences zum Speichern von Daten verwenden, sollten Sie stattdessen zu DataStore migrieren.

Preferences DataStore und Proto DataStore

DataStore bietet zwei verschiedene Implementierungen: Preferences DataStore und Proto DataStore.

  • Preferences DataStore speichert Daten mithilfe von Schlüsseln und greift darauf zu. Für diese Implementierung ist kein vordefiniertes Schema erforderlich und sie bietet keine Typsicherheit.
  • Proto DataStore speichert Daten als Instanzen eines benutzerdefinierten Datentyps. Bei dieser Implementierung müssen Sie ein Schema mit Protokollzwischenspeichern definieren, aber es bietet Typsicherheit.

DataStore richtig verwenden

Beachten Sie die folgenden Regeln, um DataStore richtig zu verwenden:

  1. Erstelle niemals mehr als eine Instanz von DataStore für eine bestimmte Datei im selben Prozess. Dadurch können alle DataStore-Funktionen beeinträchtigt werden. Wenn für eine bestimmte Datei im selben Prozess mehrere DataStores aktiv sind, wirft DataStore beim Lesen oder Aktualisieren von Daten IllegalStateException.

  2. Der generische Typ des DataStore muss unveränderlich sein. Wenn Sie einen in DataStore verwendeten Typ mutieren, werden alle von DataStore bereitgestellten Garantien ungültig und es entstehen potenziell schwer zu findende Fehler. Wir empfehlen dringend, Protokoll-Buffers zu verwenden, die für Unveränderlichkeit sorgen, eine einfache API bieten und eine effiziente Serialisierung ermöglichen.

  3. Verwenden Sie SingleProcessDataStore und MultiProcessDataStore niemals gleichzeitig für dieselbe Datei. Wenn Sie aus mehreren Prozessen auf die DataStore zugreifen möchten, verwenden Sie immer MultiProcessDataStore.

Einrichten

Wenn Sie Jetpack DataStore in Ihrer App verwenden möchten, fügen Sie Ihrer Gradle-Datei je nach gewünschter Implementierung Folgendes hinzu:

Einstellungen-Datenspeicher

Cool

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Cool

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

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

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

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

Kotlin

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

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

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

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

Schlüssel/Wert-Paare im Datenspeicher für Einstellungen speichern

Die DataStore-Implementierung für die Einstellungen verwendet die Klassen DataStore und Preferences, um einfache Schlüssel/Wert-Paare auf der Festplatte zu speichern.

Datenspeicher für Einstellungen erstellen

Verwenden Sie den Property-Delegierten, der mit preferencesDataStore erstellt wurde, um eine Instanz von DataStore<Preferences> zu erstellen. Rufen Sie sie einmal auf oberster Ebene Ihrer Kotlin-Datei auf und greifen Sie in der restlichen Anwendung über diese Property darauf zu. So lässt sich DataStore leichter als Singleton beibehalten. Alternativ können Sie RxPreferenceDataStoreBuilder verwenden, wenn Sie RxJava verwenden. Der obligatorische Parameter name ist der Name des Datenspeichers für die Einstellungen.

Kotlin

// At the top level of your kotlin file:
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")

Java

RxDataStore<Preferences> dataStore =
  new RxPreferenceDataStoreBuilder(context, /*name=*/ "settings").build();

Aus einem DataStore für Einstellungen lesen

Da im Preferences DataStore kein vordefiniertes Schema verwendet wird, müssen Sie mit der entsprechenden Funktion für den Schlüsseltyp einen Schlüssel für jeden Wert definieren, den Sie in der DataStore<Preferences>-Instanz speichern möchten. Wenn Sie beispielsweise einen Schlüssel für einen Ganzzahlwert definieren möchten, verwenden Sie intPreferencesKey(). Verwenden Sie dann die Property DataStore.data, um den entsprechenden gespeicherten Wert mit einem Flow freizugeben.

Kotlin

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

Java

Preferences.Key<Integer> EXAMPLE_COUNTER = PreferencesKeys.int("example_counter");

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

In einen Datenspeicher für Einstellungen schreiben

Der Datenspeicher für Einstellungen bietet die Funktion edit(), mit der die Daten in einem DataStore transaktional aktualisiert werden. Der Parameter transform der Funktion akzeptiert einen Codeblock, in dem Sie die Werte nach Bedarf aktualisieren können. Der gesamte Code im Transformierungsblock wird als einzelne Transaktion behandelt.

Kotlin

suspend fun incrementCounter() {
  context.dataStore.edit { settings ->
    val currentCounterValue = settings[EXAMPLE_COUNTER] ?: 0
    settings[EXAMPLE_COUNTER] = currentCounterValue + 1
  }
}

Java

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.

Typisierte Objekte mit Proto DataStore speichern

Die Proto DataStore-Implementierung verwendet DataStore und Protokoll-Buffer, um typisierte Objekte auf dem Laufwerk zu speichern.

Schema definieren

Für Proto DataStore ist ein vordefiniertes Schema in einer Proto-Datei im Verzeichnis app/src/main/proto/ erforderlich. In diesem Schema wird der Typ der Objekte definiert, die Sie in Ihrem Proto DataStore speichern. Weitere Informationen zum Definieren eines Proto-Schemas finden Sie im Leitfaden zur Protobuf-Sprache.

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Proto-Datenspeicher erstellen

Es sind zwei Schritte erforderlich, um einen Proto DataStore zum Speichern Ihrer typisierten Objekte zu erstellen:

  1. Definieren Sie eine Klasse, die Serializer<T> implementiert, wobei T der in der Protodatei definierte Typ ist. Diese Serializer-Klasse gibt DataStore an, wie der Datentyp gelesen und geschrieben werden soll. Geben Sie einen Standardwert für den Serializer an, der verwendet werden soll, wenn noch keine Datei erstellt wurde.
  2. Verwenden Sie den von dataStore erstellten Property Delegate, um eine Instanz von DataStore<T> zu erstellen. Dabei ist T der in der Protodatei definierte Typ. Rufen Sie diese Funktion einmal auf oberster Ebene Ihrer Kotlin-Datei auf und greifen Sie in der restlichen App über diesen Property Delegate darauf zu. Der Parameter filename gibt DataStore an, welche Datei zum Speichern der Daten verwendet werden soll, und der Parameter serializer gibt DataStore den Namen der in Schritt 1 definierten Serializer-Klasse an.

Kotlin

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
)

Java

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();

Aus einem Proto DataStore lesen

Mit DataStore.data kannst du einen Flow der entsprechenden Property aus deinem gespeicherten Objekt freigeben.

Kotlin

val exampleCounterFlow: Flow<Int> = context.settingsDataStore.data
  .map { settings ->
    // The exampleCounter property is generated from the proto schema.
    settings.exampleCounter
  }

Java

Flowable<Integer> exampleCounterFlow =
  dataStore.data().map(settings -> settings.getExampleCounter());

In einen Proto DataStore schreiben

Proto DataStore bietet eine updateData()-Funktion, mit der ein gespeichertes Objekt transaktionsweise aktualisiert wird. updateData() gibt den aktuellen Status der Daten als Instanz Ihres Datentyps zurück und aktualisiert die Daten transaktional in einem atomaren Lese-Schreib-Änderungsvorgang.

Kotlin

suspend fun incrementCounter() {
  context.settingsDataStore.updateData { currentSettings ->
    currentSettings.toBuilder()
      .setExampleCounter(currentSettings.exampleCounter + 1)
      .build()
    }
}

Java

Single<Settings> updateResult =
  dataStore.updateDataAsync(currentSettings ->
    Single.just(
      currentSettings.toBuilder()
        .setExampleCounter(currentSettings.getExampleCounter() + 1)
        .build()));

DataStore in synchronem Code verwenden

Einer der Hauptvorteile von DataStore ist die asynchrone API. Es ist jedoch nicht immer möglich, den umgebenden Code asynchron zu ändern. Das kann der Fall sein, wenn Sie mit einer vorhandenen Codebasis arbeiten, die synchrone Laufwerk-E/A verwendet, oder wenn Sie eine Abhängigkeit haben, die keine asynchrone API bereitstellt.

Kotlin-Goroutinen bieten den runBlocking()-Goroutinen-Builder, um die Lücke zwischen synchronem und asynchronem Code zu schließen. Mit runBlocking() können Sie Daten synchron aus Datastore lesen. RxJava bietet Blockierungsmethoden für Flowable. Im folgenden Code wird der aufrufende Thread blockiert, bis DataStore Daten zurückgibt:

Kotlin

val exampleData = runBlocking { context.dataStore.data.first() }

Java

Settings settings = dataStore.data().blockingFirst();

Die Ausführung synchroner E/A-Vorgänge im UI-Thread kann zu ANRs oder Rucklern der Benutzeroberfläche führen. Sie können diese Probleme vermeiden, indem Sie die Daten aus dem DataStore asynchron vorladen:

Kotlin

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

Java

dataStore.data().first().subscribe();

So liest DataStore die Daten asynchron und speichert sie im Arbeitsspeicher. Spätere synchrone Lesevorgänge mit runBlocking() können schneller sein oder eine Laufwerk-I/O-Operation vollständig vermeiden, wenn die anfängliche Lesevorgang abgeschlossen ist.

DataStore in Code mit mehreren Prozessen verwenden

Sie können DataStore so konfigurieren, dass in verschiedenen Prozessen auf dieselben Daten zugegriffen wird, wobei die Datenkonsistenz genauso gewährleistet ist wie innerhalb eines einzelnen Prozesses. Insbesondere garantiert DataStore Folgendes:

  • Bei Lesezugriffen werden nur die Daten zurückgegeben, die auf dem Laufwerk gespeichert wurden.
  • Konsistenz beim Lesen nach dem Schreiben.
  • Schreibvorgänge werden serialisiert.
  • Lesevorgänge werden nie durch Schreibvorgänge blockiert.

Betrachten Sie eine Beispielanwendung mit einem Dienst und einer Aktivität:

  1. Der Dienst wird in einem separaten Prozess ausgeführt und aktualisiert den DataStore regelmäßig.

    <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. Die App würde diese Änderungen erfassen und die Benutzeroberfläche aktualisieren.

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

Damit DataStore in verschiedenen Prozessen verwendet werden kann, müssen Sie das DataStore-Objekt mithilfe der MultiProcessDataStoreFactory erstellen.

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

serializer gibt DataStore an, wie der Datentyp gelesen und geschrieben werden soll. Geben Sie einen Standardwert für den Serializer an, der verwendet werden soll, wenn noch keine Datei erstellt wurde. Unten finden Sie ein Beispiel für die Implementierung mit 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()
       )
   }
}

Mit der Hilt-Abhängigkeitsinjektion können Sie dafür sorgen, dass Ihre DataStore-Instanz pro Prozess eindeutig ist:

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

Umgang mit beschädigten Dateien

In seltenen Fällen kann die persistente Datei von DataStore auf dem Laufwerk beschädigt werden. Standardmäßig wird DataStore bei Beschädigung nicht automatisch wiederhergestellt. Versuche, Daten daraus zu lesen, führen dazu, dass das System eine CorruptionException auslöst.

DataStore bietet eine API für den Umgang mit Beschädigungen, mit der Sie in einem solchen Fall eine ordnungsgemäße Wiederherstellung durchführen und das Auslösen der Ausnahme vermeiden können. Wenn der Datenbeschädigungs-Handler konfiguriert ist, ersetzt er die beschädigte Datei durch eine neue Datei mit einem vordefinierten Standardwert.

Wenn Sie diesen Handler einrichten möchten, geben Sie beim Erstellen der DataStore-Instanz in by dataStore() oder in der DataStoreFactory-Factorymethode eine corruptionHandler an:

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

Feedback geben

Du kannst uns dein Feedback und deine Ideen über die folgenden Ressourcen mitteilen:

Issue Tracker
Melden Sie Probleme, damit wir Fehler beheben können.

Weitere Informationen

Weitere Informationen zu Jetpack DataStore finden Sie in den folgenden Ressourcen:

Produktproben

Blogs

Codelabs