Datastore Teil von Android Jetpack.
Jetpack Datastore ist eine Datenspeicherlösung, mit der Sie Schlüssel/Wert Paare oder typisierte Objekte mit Protokollpuffern speichern können. Datastore verwendet Kotlin-Coroutinen und Flow, um Daten asynchron, konsistent und transaktional zu speichern.
Wenn Sie SharedPreferences zum Speichern von Daten verwenden, sollten Sie stattdessen zu
Datastore migrieren.
Datastore API
Die DataStore-Schnittstelle bietet die folgende API:
Ein Flow, mit dem Daten aus dem Datastore gelesen werden können
val data: Flow<T>Eine Funktion zum Aktualisieren von Daten im Datastore
suspend updateData(transform: suspend (t) -> T)
Datastore-Konfigurationen
Wenn Sie Daten mit Schlüsseln speichern und darauf zugreifen möchten, verwenden Sie die Preferences Datastore-Implementierung. Diese erfordert kein vordefiniertes Schema und bietet keine Typsicherheit. Sie hat eine SharedPreferences-ähnliche API, aber nicht
die Nachteile, die mit freigegebenen Einstellungen verbunden sind.
Mit Datastore können Sie benutzerdefinierte Klassen beibehalten. Dazu müssen Sie ein Schema für die Daten definieren und einen Serializer bereitstellen, um sie in ein dauerhaftes Format zu konvertieren. Sie können Protokollpuffer, JSON oder eine andere Serialisierungsstrategie verwenden.
Einrichtung
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
Fügen Sie der Datei „build.gradle“ die folgenden Zeilen hinzu:
Groovy
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation "androidx.datastore:datastore-preferences:1.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-preferences-core:1.2.1" }
Kotlin
dependencies { // Preferences DataStore (SharedPreferences like APIs) implementation("androidx.datastore:datastore-preferences:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-preferences-core:1.2.1") }
Wenn Sie die optionale RxJava-Unterstützung hinzufügen möchten, fügen Sie die folgenden Abhängigkeiten hinzu:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-preferences-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-preferences-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-preferences-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-preferences-rxjava3:1.2.1") }
DataStore
Fügen Sie der Datei „build.gradle“ die folgenden Zeilen hinzu:
Groovy
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation "androidx.datastore:datastore:1.2.1" // Alternatively - without an Android dependency. implementation "androidx.datastore:datastore-core:1.2.1" }
Kotlin
dependencies { // Typed DataStore for custom data objects (for example, using Proto or JSON). implementation("androidx.datastore:datastore:1.2.1") // Alternatively - without an Android dependency. implementation("androidx.datastore:datastore-core:1.2.1") }
Fügen Sie die folgenden optionalen Abhängigkeiten für die RxJava-Unterstützung hinzu:
Groovy
dependencies { // optional - RxJava2 support implementation "androidx.datastore:datastore-rxjava2:1.2.1" // optional - RxJava3 support implementation "androidx.datastore:datastore-rxjava3:1.2.1" }
Kotlin
dependencies { // optional - RxJava2 support implementation("androidx.datastore:datastore-rxjava2:1.2.1") // optional - RxJava3 support implementation("androidx.datastore:datastore-rxjava3:1.2.1") }
Wenn Sie Inhalte serialisieren möchten, fügen Sie Abhängigkeiten für die Protokollzwischenspeicher- oder JSON-Serialisierung hinzu.
JSON-Serialisierung
Wenn Sie die JSON-Serialisierung verwenden möchten, fügen Sie Ihrer Gradle-Datei Folgendes hinzu:
Groovy
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0" }
Kotlin
plugins { id("org.jetbrains.kotlin.plugin.serialization") version "2.2.20" } dependencies { implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.9.0") }
Protobuf-Serialisierung
Wenn Sie die Protobuf-Serialisierung verwenden möchten, fügen Sie Ihrer Gradle-Datei Folgendes hinzu:
Groovy
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation "com.google.protobuf:protobuf-kotlin-lite:4.32.1" } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
Kotlin
plugins { id("com.google.protobuf") version "0.9.5" } dependencies { implementation("com.google.protobuf:protobuf-kotlin-lite:4.32.1") } protobuf { protoc { artifact = "com.google.protobuf:protoc:4.32.1" } generateProtoTasks { all().forEach { task -> task.builtins { create("java") { option("lite") } create("kotlin") } } } }
Datastore richtig verwenden
Beachten Sie bei der Verwendung von Datastore immer die folgenden Regeln:
Erstellen Sie niemals mehr als eine Instanz von
DataStorefür eine bestimmte Datei in demselben Prozess. Andernfalls kann die gesamte Datastore-Funktionalität beeinträchtigt werden. Wenn im selben Prozess mehrere Datastores für eine bestimmte Datei aktiv sind, löst Datastore beim Lesen oder Aktualisieren von DatenIllegalStateExceptionaus.Der generische Typ von
DataStore<T>muss unveränderlich sein. Wenn Sie einen in Datastore verwendeten Typ ändern, wird die von Datastore bereitgestellte Konsistenz ungültig und es können potenziell schwerwiegende, schwer zu findende Fehler entstehen. Wir empfehlen die Verwendung von Protokollpuffern, die für Unveränderlichkeit, eine klare API und eine effiziente Serialisierung sorgen.Verwenden Sie nicht
SingleProcessDataStoreundMultiProcessDataStorefür dieselbe Datei. Wenn Sie von mehr als einem Prozess auf denDataStorezugreifen möchten, müssen SieMultiProcessDataStoreverwenden.
Datendefinition
Preferences Datastore
Definieren Sie einen Schlüssel, der verwendet wird, um Daten auf dem Laufwerk zu speichern.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
JSON Datastore
Fügen Sie für JSON Datastore eine @Serialization-Annotation zu den Daten hinzu, die Sie beibehalten möchten.
@Serializable data class Settings(val exampleCounter: Int)
Definieren Sie eine Klasse, die Serializer<T> implementiert, wobei T der Typ der
Klasse ist, zu der Sie die vorherige Annotation hinzugefügt haben. Fügen Sie einen Standardwert für den Serializer hinzu, der verwendet werden soll, wenn noch keine Datei erstellt wurde.
object SettingsSerializer : Serializer<Settings> { override val defaultValue: Settings = Settings(exampleCounter = 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()) } }
Proto Datastore
Die Proto Datastore-Implementierung verwendet Datastore und Protokollpuffer, um typisierte Objekte auf dem Laufwerk zu speichern.
Für Proto Datastore ist ein vordefiniertes Schema in einer Proto-Datei im Verzeichnis app/src/main/proto/ erforderlich. Dieses Schema definiert den Typ für die Objekte, die Sie in Ihrem Proto Datastore beibehalten. Weitere Informationen zum Definieren eines Proto
Schemas finden Sie im Leitfaden zur Protokollpuffer-Sprache.
Fügen Sie im Ordner src/main/proto eine Datei mit dem Namen settings.proto hinzu:
syntax = "proto3"; option java_package = "com.example.datastoresampleapp"; option java_multiple_files = true; message Settings { int32 counter = 1; bool foo = 2; }
Definieren Sie eine Klasse, die Serializer<T> implementiert, wobei T der in der Proto-Datei definierte Typ ist. Diese Serializer-Klasse definiert, wie Datastore Ihren Datentyp liest und schreibt. Fügen Sie einen Standardwert für den Serializer hinzu, der verwendet werden soll, wenn noch keine Datei erstellt wurde.
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) { return t.writeTo(output) } }
Datastore erstellen
Sie müssen einen Namen für die Datei angeben, die zum Speichern der Daten verwendet wird.
Preferences Datastore
Die Preferences Datastore-Implementierung verwendet die DataStore und
Preferences Klassen, um Schlüssel/Wert-Paare auf dem Laufwerk zu speichern. Verwenden Sie den
Eigenschaftsdelegaten, der von preferencesDataStore erstellt wurde, um eine Instanz
von DataStore<Preferences> zu erstellen. Rufen Sie ihn einmal auf der obersten Ebene Ihrer Kotlin-Datei auf. Greifen Sie über diese Eigenschaft auf den Datastore zu. So können Sie Ihren Datastore einfacher als Singleton beibehalten.
Der obligatorische Parameter name ist der Name des Preferences Datastore.
// At the top level of your kotlin file: val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = "settings")
JSON Datastore
Verwenden Sie den von dataStore erstellten Eigenschaftsdelegaten, um eine Instanz von
DataStore<T> zu erstellen, wobei T die serialisierbare Datenklasse ist. Rufen Sie ihn einmal auf der obersten Ebene Ihrer Kotlin-Datei auf und greifen Sie über diesen Eigenschaftsdelegaten auf den Rest Ihrer App zu. Der Parameter fileName gibt an, welche Datei zum Speichern der Daten verwendet werden soll, und der Parameter serializer gibt den Namen der zuvor definierten Serializer-Klasse an.
val Context.dataStore: DataStore<Settings> by dataStore( fileName = "settings.json", serializer = SettingsSerializer, scope = CoroutineScope(Dispatchers.IO + SupervisorJob()), )
Proto Datastore
Verwenden Sie den von dataStore erstellten Eigenschaftsdelegaten, um eine Instanz von
DataStore<T> zu erstellen, wobei T der in der Proto-Datei definierte Typ ist. Rufen Sie ihn einmal auf der obersten Ebene Ihrer Kotlin-Datei auf und greifen Sie über diesen Eigenschaftsdelegaten auf den Rest Ihrer App zu. Der Parameter fileName gibt an, welche Datei zum Speichern der Daten verwendet werden soll, und der Parameter serializer gibt den Namen der zuvor definierten Serializer-Klasse an.
val Context.dataStore: DataStore<Settings> by dataStore(fileName = "settings.pb", serializer = SettingsSerializer)
Aus Datastore lesen
Sie müssen einen Namen für die Datei angeben, die zum Speichern der Daten verwendet wird.
Preferences Datastore
Da Preferences Datastore kein vordefiniertes Schema verwendet, müssen Sie
die entsprechende Funktion für den Schlüsseltyp verwenden, um einen Schlüssel für jeden Wert zu definieren, den Sie
in der DataStore<Preferences> Instanz speichern möchten. Verwenden Sie beispielsweise
einen Schlüssel für einen Integerwert zu definieren, verwenden Sie intPreferencesKey. Verwenden Sie dann die
DataStore.data Eigenschaft, um den entsprechenden gespeicherten Wert mit einem
Flow verfügbar zu machen.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { preferences -> preferences[EXAMPLE_COUNTER] ?: 0 }
JSON Datastore
Verwenden Sie DataStore.data, um einen Flow der entsprechenden Eigenschaft aus Ihrem gespeicherten Objekt verfügbar zu machen.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings -> settings.exampleCounter }
Proto Datastore
Verwenden Sie DataStore.data, um einen Flow der entsprechenden Eigenschaft aus Ihrem gespeicherten Objekt verfügbar zu machen.
fun counterFlow(): Flow<Int> = context.dataStore.data.map { settings -> settings.counter }
Verwenden Sie collectAsStateWithLifecycle, um den Flow zu verwenden, der von
einem ViewModel in einem zusammensetzbaren Element erzeugt wird.
Dadurch wird der Datastore-Flow sicher in einen Compose-Zustand konvertiert, der eine Neukomposition auslöst.
@Composable
fun SomeScreen(counterFlow: Flow<Int>) {
val counter by counterFlow.collectAsStateWithLifecycle(initialValue = 0)
Text(text = "Example counter: ${counter}")
}
Weitere Informationen zu collectAsStateWithLifecycle,
finden Sie unter Status und Jetpack Compose.
Schreibzugriff auf Datastore
Datastore bietet eine `updateData`-Funktion, mit der ein
gespeichertes Objekt transaktional 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- und Änderungsvorgang. Der gesamte Code im updateData-Block wird als eine einzelne Transaktion behandelt.
Preferences Datastore
suspend fun incrementCounter() { context.dataStore.updateData { it.toMutablePreferences().also { preferences -> preferences[EXAMPLE_COUNTER] = (preferences[EXAMPLE_COUNTER] ?: 0) + 1 } } }
JSON Datastore
suspend fun incrementCounter() { context.dataStore.updateData { settings -> settings.copy(exampleCounter = settings.exampleCounter + 1) } }
Proto Datastore
suspend fun incrementCounter() { context.dataStore.updateData { settings -> settings.toBuilder().setCounter(settings.counter + 1).build() } }
Datastore in einer Compose-App verwenden
Wenn Sie Datastore in einer Compose-App verwenden möchten, folgen Sie den Richtlinien für die Android-App-Architektur. Bewahren Sie Datastore-Vorgänge in Ihrer Datenschicht (z. B. einem Repository) auf und machen Sie die Daten über ein ViewModel für Ihre UI verfügbar.
Vermeiden Sie es, direkt in Ihren zusammensetzbaren Funktionen aus Datastore zu lesen oder in Datastore zu schreiben.
Datastore über ein ViewModel verfügbar machen. Übergeben Sie Ihr Repository (das den Datastore umschließt) an Ihr
ViewModelund konvertieren Sie denFlowin einenStateFlow, damit die UI ihn beobachten kann, wie in dem folgenden Snippet gezeigt:class SettingsViewModel( private val userPreferencesRepository: UserPreferencesRepository ) : ViewModel() { // Expose the DataStore flow as a StateFlow for Compose val userSettings: StateFlow<UserSettings> = userPreferencesRepository.userSettingsFlow .stateIn( scope = viewModelScope, started = SharingStarted.WhileSubscribed(5000), initialValue = UserSettings.getDefaultInstance() ) fun updateCounter(newValue: Int) { viewModelScope.launch { userPreferencesRepository.updateCounter(newValue) } } }Aus Ihrem zusammensetzbaren Element beobachten und schreiben. Verwenden Sie
collectAsStateWithLifecycle, um denStateFlowsicher in Ihrer UI zu beobachten, und rufen Sie dieViewModel-Funktionen auf, um Schreibvorgänge zu verarbeiten, wie im folgenden Snippet gezeigt:@Composable fun SettingsScreen( viewModel: SettingsViewModel = viewModel() ) { // Safely collect the state val settings by viewModel.userSettings.collectAsStateWithLifecycle() Column(modifier = Modifier.padding(16.dp)) { Text(text = "Current counter: ${settings.counter}") Spacer(modifier = Modifier.height(8.dp)) Button(onClick = { viewModel.updateCounter(settings.counter + 1) }) { Text("Increment Counter") } } }
Datastore in Code mit mehreren Prozessen verwenden
Sie können Datastore so konfigurieren, dass auf dieselben Daten in verschiedenen Prozessen mit denselben Datenkonsistenzeigenschaften wie in einem einzelnen Prozess zugegriffen wird. Datastore bietet insbesondere die folgenden Eigenschaften:
- Lesevorgänge geben nur die Daten zurück, die auf dem Laufwerk 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, wobei der Dienst in einem separaten Prozess ausgeführt wird und den Datastore regelmäßig aktualisiert.
In diesem Beispiel wird ein JSON-Datastore verwendet, Sie können aber auch einen Preferences- oder Proto-Datastore verwenden.
@Serializable data class Time(val lastUpdateMillis: Long)
Ein Serializer gibt DataStore an, wie Ihr Datentyp gelesen und geschrieben werden soll. Fügen Sie einen Standardwert für den Serializer hinzu, der verwendet werden soll, wenn noch keine Datei erstellt wurde. Im Folgenden finden Sie eine Beispielimplementierung mit
kotlinx.serialization:
object TimeSerializer : Serializer<Time> { override val defaultValue: Time = Time(lastUpdateMillis = 0L) override suspend fun readFrom(input: InputStream): Time = try { Json.decodeFromString(Time.serializer(), input.readBytes().decodeToString()) } catch (serialization: SerializationException) { throw CorruptionException("Unable to read Time", serialization) } override suspend fun writeTo(t: Time, output: OutputStream) { output.write(Json.encodeToString(Time.serializer(), t).encodeToByteArray()) } }
Damit Sie DataStore in verschiedenen Prozessen verwenden können, müssen Sie
das Datastore-Objekt mit MultiProcessDataStoreFactory für den App
und den Dienstcode erstellen:
val dataStore = MultiProcessDataStoreFactory.create( serializer = TimeSerializer, produceFile = { context.dataStoreFile("time.pb") }, corruptionHandler = null, )
Fügen Sie AndroidManifiest.xml Folgendes hinzu:
<service android:name="com.example.datastore.snippets.TimestampUpdateService" android:exported="false" android:process=":service" />
Der Dienst ruft regelmäßig updateLastUpdateTime auf, wodurch mit updateData in den Datastore geschrieben wird.
suspend fun updateLastUpdateTime() { dataStore.updateData { time -> time.copy(lastUpdateMillis = System.currentTimeMillis()) } }
Die App liest den vom Dienst geschriebenen Wert mit dem Datenfluss:
fun timeFlow(): Flow<Long> = dataStore.data.map { time -> time.lastUpdateMillis }
Jetzt können wir alle diese Funktionen in einer Klasse namens MultiProcessDataStore zusammenfassen und in einer App verwenden.
Hier ist der Dienstcode:
class TimestampUpdateService : Service() { val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.IO) val multiProcessDataStore by lazy { MultiProcessDataStore(applicationContext) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { serviceScope.launch { while (true) { multiProcessDataStore.updateLastUpdateTime() delay(1000) } } return START_NOT_STICKY } override fun onDestroy() { super.onDestroy() serviceScope.cancel() } }
Und der App-Code:
val context = LocalContext.current val coroutineScope = rememberCoroutineScope() val multiProcessDataStore = remember(context) { MultiProcessDataStore(context) } // Display time written by other process. val lastUpdateTime by multiProcessDataStore .timeFlow() .collectAsState(initial = 0, coroutineScope.coroutineContext) Text(text = "Last updated: $lastUpdateTime", fontSize = 25.sp) DisposableEffect(context) { val serviceIntent = Intent(context, TimestampUpdateService::class.java) context.startService(serviceIntent) onDispose { context.stopService(serviceIntent) } }
Sie können die Hilt-Abhängigkeitsinjektion verwenden, damit Ihre Datastore -Instanz pro Prozess eindeutig ist:
@Provides
@Singleton
fun provideDataStore(@ApplicationContext context: Context): DataStore<Settings> =
MultiProcessDataStoreFactory.create(...)
Dateibeschädigung verarbeiten
In seltenen Fällen kann die dauerhafte Datei von Datastore auf dem Laufwerk beschädigt werden. Standardmäßig wird die Beschädigung von Datastore nicht automatisch behoben. Wenn Sie versuchen, daraus zu lesen, löst das System eine CorruptionException aus.
Datastore bietet eine API für die Beschädigungsbehandlung, mit der Sie sich in einem solchen Szenario ordnungsgemäß erholen und die Ausnahme vermeiden können. Wenn konfiguriert, ersetzt die Beschädigungsbehandlung die beschädigte Datei durch eine neue Datei mit einem vordefinierten Standardwert.
Um diese Behandlung einzurichten, geben Sie beim Erstellen der Datastore-Instanz in by dataStore oder in der Factory-Methode DataStoreFactory einen corruptionHandler an:
val dataStore: DataStore<Settings> = DataStoreFactory.create(
serializer = SettingsSerializer(),
produceFile = {
File("${context.filesDir.path}/myapp.preferences_pb")
},
corruptionHandler = ReplaceFileCorruptionHandler { Settings(lastUpdate = 0) }
)
Gerätesicherung und ‑wiederherstellung
Preferences Datastore-Dateien (*.preferences_pb) und Proto Datastore-Dateien werden im Verzeichnis files/datastore/ der App gespeichert. Standardmäßig sind diese Dateien
in der automatischen Android-Sicherung und bei Gerät-zu-Gerät-Übertragungen (D2D) enthalten.
Sicherungsregeln für Datastore konfigurieren
Wenn Ihr Datastore neben sensiblen Daten auch nicht sensible Einstellungen (z. B. Nutzerdesign oder Feature-Flags) enthält, trennen Sie sie in separate Datastore-Dateien auf und konfigurieren Sie res/xml/data_extraction_rules.xml:
<data-extraction-rules>
<cloud-backup>
<!-- Include general settings -->
<include domain="file" path="datastore/user_settings.preferences_pb"/>
<!-- Exclude sensitive local state -->
<exclude domain="file" path="datastore/secure_state.preferences_pb"/>
</cloud-backup>
<device-to-device>
<!-- Transfer settings during device-to-device transfer -->
<include domain="file" path="datastore/"/>
</device-to-device>
</data-extraction-rules>
Feedback geben
Teilen Sie uns Ihr Feedback und Ihre Ideen über diese Ressourcen mit:
- Problemverfolgung:
- Melden Sie Probleme, damit wir Fehler beheben können.
Zusätzliche Ressourcen
Weitere Informationen zu Jetpack Datastore finden Sie in den folgenden zusätzlichen Ressourcen:
Beispiele
Blogs
Codelabs
Empfehlungen für Sie
- Hinweis: Linktext wird angezeigt, wenn JavaScript deaktiviert ist
- Paginierte Daten laden und anzeigen
- LiveData – Übersicht
- Layouts und Bindungsausdrücke