Datastore   Teil von Android Jetpack.

Mit Kotlin Multiplatform testen
Mit Kotlin Multiplatform kann die Datenschicht für andere Plattformen freigegeben werden. Informationen zum Einrichten und Verwenden von Datastore in KMP

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:

  1. Ein Flow, mit dem Daten aus dem Datastore gelesen werden können

    val data: Flow<T>
    
  2. 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:

  1. 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 Daten IllegalStateException aus.

  2. 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.

  3. Verwenden Sie nicht gleichzeitig SingleProcessDataStore und MultiProcessDataStore für dieselbe Datei. Wenn Sie von mehr als einem Prozess auf das DataStore zugreifen möchten, müssen Sie MultiProcessDataStore verwenden.

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.

  1. Datastore über ein ViewModel verfügbar machen. Übergeben Sie Ihr Repository (das den Datastore umschließt) an Ihr ViewModel und konvertieren Sie den Flow in einen StateFlow, 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)
            }
        }
    }
    
  2. In Ihrem Compositable beobachten und schreiben. Verwenden Sie collectAsStateWithLifecycle, um den StateFlow sicher in Ihrer UI zu beobachten, und rufen Sie die ViewModel-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