DataStore   Teil von Android Jetpack.

Mit Kotlin Multiplatform testen
Mit Kotlin Multiplatform kann die Datenschicht für andere Plattformen freigegeben werden. DataStore in KMP einrichten und verwenden

Jetpack DataStore ist eine Datenspeicherlösung, mit der Sie Schlüssel/Wert-Paare oder typisierte Objekte mit Protocol 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 eine Migration zu DataStore in Betracht ziehen.

Preferences DataStore und Proto DataStore

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

  • Im Preferences DataStore werden Daten mithilfe von Schlüsseln gespeichert und abgerufen. Für diese Implementierung ist kein vordefiniertes Schema erforderlich und sie bietet keine Typsicherheit.
  • In Proto DataStore werden Daten als Instanzen eines benutzerdefinierten Datentyps gespeichert. Bei dieser Implementierung müssen Sie ein Schema mit Protokollpuffern definieren, sie bietet jedoch Typsicherheit.

DataStore richtig verwenden

Damit Sie DataStore richtig verwenden, sollten Sie immer die folgenden Regeln beachten:

  1. Erstellen Sie niemals mehr als eine Instanz von DataStore für eine bestimmte Datei im selben Prozess. Dadurch kann die gesamte DataStore-Funktionalität beeinträchtigt werden. Wenn für eine bestimmte Datei im selben Prozess mehrere DataStores aktiv sind, wird beim Lesen oder Aktualisieren von Daten eine IllegalStateException ausgelöst.

  2. Der generische Typ von DataStore<T> muss unveränderlich sein. Wenn Sie einen in DataStore verwendeten Typ ändern, werden alle von DataStore bereitgestellten Garantien ungültig und es können potenziell schwerwiegende, schwer zu erkennende Fehler auftreten. Es wird dringend empfohlen, Protokollzwischenspeicher zu verwenden, da sie Unveränderlichkeitsgarantien, eine einfache API und eine effiziente Serialisierung bieten.

  3. Verwenden Sie für dieselbe Datei niemals eine Mischung aus SingleProcessDataStore und MultiProcessDataStore. Wenn Sie von mehr als einem Prozess auf 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:

Preferences DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

Proto DataStore

Groovy

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

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

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

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

Kotlin

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

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

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

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

Schlüssel/Wert-Paare mit Preferences DataStore speichern

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

Preferences DataStore erstellen

Verwenden Sie den von preferencesDataStore erstellten Property-Delegaten, um eine Instanz von DataStore<Preferences> zu erstellen. Rufen Sie sie einmal auf der obersten Ebene Ihrer Kotlin-Datei auf und greifen Sie über diese Eigenschaft auf den Rest Ihrer Anwendung zu. So können Sie DataStore leichter als Singleton beibehalten. Wenn Sie RxJava verwenden, können Sie stattdessen RxPreferenceDataStoreBuilder verwenden. Der obligatorische Parameter name ist der Name des Preferences DataStore.

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 Preferences DataStore lesen

Da Preferences DataStore kein vordefiniertes Schema verwendet, 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 Int-Wert definieren möchten, verwenden Sie intPreferencesKey(). Verwenden Sie dann die Property DataStore.data, um den entsprechenden gespeicherten Wert über eine Flow verfügbar zu machen.

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 Preferences DataStore schreiben

Preferences DataStore bietet die Funktion edit(), mit der die Daten in einem DataStore transaktional aktualisiert werden. Der transform-Parameter der Funktion akzeptiert einen Codeblock, in dem Sie die Werte nach Bedarf aktualisieren können. Der gesamte Code im Transformationsblock wird als eine 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

Bei der Proto DataStore-Implementierung werden DataStore und Protobufers verwendet, um typisierte Objekte auf der Festplatte zu speichern.

Schema definieren

Für Proto DataStore ist ein vordefiniertes Schema in einer Protobuf-Datei im Verzeichnis app/src/main/proto/ erforderlich. Dieses Schema definiert den Typ für die Objekte, die Sie in Ihrem Proto DataStore speichern. Weitere Informationen zum Definieren eines Proto-Schemas finden Sie im protobuf-Sprachleitfaden.

syntax = "proto3";

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

message Settings {
  int32 example_counter = 1;
}

Proto DataStore erstellen

Es gibt zwei Schritte zum Erstellen eines Proto DataStore zum Speichern Ihrer typisierten Objekte:

  1. Definieren Sie eine Klasse, die Serializer<T> implementiert, wobei T der in der Proto-Datei definierte Typ ist. Diese Serializer-Klasse gibt DataStore an, wie Ihr Datentyp gelesen und geschrieben werden soll. Achten Sie darauf, dass Sie einen Standardwert für den Serializer angeben, der verwendet werden soll, wenn noch keine Datei erstellt wurde.
  2. Verwenden Sie den von dataStore erstellten Attribut-Delegate, um eine Instanz von DataStore<T> zu erstellen, wobei T der in der Proto-Datei definierte Typ ist. Rufen Sie diese einmal auf der obersten Ebene Ihrer Kotlin-Datei auf und greifen Sie über diese Eigenschaftsdelegierung im Rest Ihrer App darauf zu. Der Parameter filename gibt an, welche Datei DataStore zum Speichern der Daten verwenden soll, und der Parameter serializer gibt den Namen der Serializer-Klasse an, die in Schritt 1 definiert wurde.

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

Verwenden Sie DataStore.data, um eine Flow der entsprechenden Eigenschaft aus dem gespeicherten Objekt verfügbar zu machen.

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 die Funktion updateData(), mit der ein gespeichertes Objekt transaktional aktualisiert wird. updateData() gibt Ihnen 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 möglicherweise nicht immer möglich, den umgebenden Code in asynchronen Code zu ändern. Das kann der Fall sein, wenn Sie mit einer vorhandenen Codebasis arbeiten, die synchrone Laufwerk-I/O verwendet, oder wenn Sie eine Abhängigkeit haben, die keine asynchrone API bietet.

Kotlin-Coroutinen bieten den Coroutinen-Builder runBlocking(), um die Lücke zwischen synchronem und asynchronem Code zu schließen. Mit runBlocking() können Sie Daten synchron aus DataStore lesen. RxJava bietet blockierende Methoden für Flowable. Die folgenden Codeblöcke blockieren den aufrufenden Thread, bis DataStore Daten zurückgibt:

Kotlin

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

Java

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

Wenn Sie synchrone E/A-Vorgänge im UI-Thread ausführen, kann dies zu ANR-Fehlern oder Ruckeln der Benutzeroberfläche führen. Sie können diese Probleme umgehen, indem Sie die Daten aus DataStore asynchron vorab laden:

Kotlin

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

Java

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

Auf diese Weise liest DataStore die Daten asynchron und speichert sie im Cache. Spätere synchrone Lesevorgänge mit runBlocking() können schneller sein oder eine Datenträger-E/A-Operation ganz vermeiden, wenn der erste 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. Dabei gelten dieselben Garantien für die Datenkonsistenz wie in einem einzelnen Prozess. DataStore garantiert insbesondere Folgendes:

  • Bei Lesevorgängen werden nur die Daten zurückgegeben, die auf der Festplatte gespeichert wurden.
  • Read-After-Write-Konsistenz.
  • 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. Während die App diese Änderungen erfasst und die Benutzeroberfläche aktualisiert

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

Damit Sie DataStore in verschiedenen Prozessen verwenden können, müssen Sie das DataStore-Objekt mit MultiProcessDataStoreFactory erstellen.

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

serializer gibt DataStore an, wie Ihr Datentyp gelesen und geschrieben werden soll. Achten Sie darauf, dass Sie einen Standardwert für den Serializer angeben, der verwendet werden soll, wenn noch keine Datei erstellt wurde. Unten sehen 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): Settings =
       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 auf dem Datenträger von DataStore beschädigt werden. Standardmäßig wird bei DataStore nicht automatisch versucht, beschädigte Daten wiederherzustellen. Leseversuche führen dazu, dass das System eine CorruptionException auslöst.

DataStore bietet eine API für die Verarbeitung von Beschädigungen, mit der Sie sich in einem solchen Fall ordnungsgemäß erholen und das Auslösen der Ausnahme vermeiden können. Wenn der Handler für beschädigte Dateien konfiguriert ist, ersetzt er die beschädigte Datei durch eine neue Datei mit einem vordefinierten Standardwert.

Um diesen Handler einzurichten, geben Sie beim Erstellen der DataStore-Instanz in by dataStore() oder in der Factory-Methode DataStoreFactory ein 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

Über diese Ressourcen können Sie uns Feedback und Ideen mitteilen:

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

Zusätzliche Ressourcen

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

Produktproben

Blogs

Codelabs