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 für eine bestimmte Datei im selben Prozess nie mehr als eine Instanz von
DataStore. 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 übersichtliche API und eine effiziente Serialisierung sorgen.Verwenden Sie nicht gleichzeitig
SingleProcessDataStoreundMultiProcessDataStorefür dieselbe Datei. Wenn Sie von mehr als einem Prozess auf dasDataStorezugreifen möchten, müssen SieMultiProcessDataStoreverwenden.
Datendefinition
Preferences DataStore
Definieren Sie einen Schlüssel, der zum Speichern von Daten auf dem Laufwerk verwendet wird.
val EXAMPLE_COUNTER = intPreferencesKey("example_counter")
JSON DataStore
Fügen Sie für JSON-Datastore der zu speichernden Daten eine @Serialization-Annotation hinzu.
@Serializable
data class Settings(
val exampleCounter: Int
)
Definieren Sie eine Klasse, die Serializer<T> implementiert, wobei T der Typ der
Klasse ist, 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>(
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(t)
.encodeToByteArray()
)
}
}
Proto DataStore
Die Proto DataStore-Implementierung verwendet Datastore und Protokollpuffer, um typisierte Objekte auf der Festplatte 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 speichern. 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.datastore.snippets.proto";
option java_multiple_files = true;
message Settings {
int32 example_counter = 1;
}
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 der Festplatte 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 Rest Ihrer Anwendung auf 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,
)
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üssen. 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.exampleCounter
}
Verwenden Sie collectAsStateWithLifecycle, um den Flow zu verwenden, der von
einem ViewModel in einem Compositable erzeugt wird.
Dadurch wird der Datastore-Flow sicher in einen Compose-Status 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-/Änderungsvorgang. Der gesamte Code im updateData-Block wird als eine einzige 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.copy { exampleCounter = exampleCounter + 1 }
}
}
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, indem Sie Datastore-Vorgänge in Ihrer Datenschicht (z. B. einem Repository) beibehalten und die Daten über ein ViewModel für die UI verfügbar machen.
Vermeiden Sie es, direkt in Ihren Compositable-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 einfach beobachten kann, wie in folgendem 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) } } }In Ihrem Compositable 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 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, 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>(
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(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 = {
File("${context.filesDir.path}/time.pb")
},
corruptionHandler = null
)
Fügen Sie AndroidManifiest.xml Folgendes hinzu:
<service
android:name=".TimestampUpdateService"
android:process=":my_process_id" />
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(...)
Umgang mit beschädigten Dateien
In seltenen Fällen kann die dauerhafte Datei von Datastore auf der Festplatte 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 den Umgang mit Beschädigungen, mit der Sie sich in einem solchen Fall ordnungsgemäß erholen und die Ausnahme vermeiden können. Wenn konfiguriert, ersetzt der Handler für Beschädigungen die beschädigte Datei durch eine neue mit einem vordefinierten Standardwert.
Um diesen Handler 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) }
)
Feedback geben
Teilen Sie uns Ihr Feedback und Ihre Ideen über diese Ressourcen mit:
- Issue Tracker:
- 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