Preferences DataStore
Preferences DataStore speichert einfache Einstellungen asynchron. Du lernst, wie Flow daraus verlässliche UI-Daten macht.
Preferences DataStore ist die moderne Jetpack-Lösung für einfache App-Einstellungen, die du als Key-Value-Paare speichern und als Flow beobachten kannst. Für dich ist das besonders wichtig, sobald deine App kleine Entscheidungen merken soll: Dark Mode, Sortierung, Onboarding gesehen, bevorzugte Sprache, Sync nur über WLAN oder ein zuletzt ausgewählter Filter. Der Kern ist nicht nur das Speichern, sondern die saubere Verbindung zur UI: Änderungen werden asynchron geliefert, passen zu Coroutines und Flow und lassen sich gut in eine Data Layer einordnen.
Was ist das?
Preferences DataStore ist ein persistenter Speicher für einfache Schlüssel-Wert-Daten. Du legst also einen Schlüssel fest, zum Beispiel dark_mode_enabled, und speicherst dazu einen Wert wie true oder false. Anders als bei einer relationalen Datenbank beschreibst du keine Tabellen, Beziehungen oder Abfragen. Du speicherst kleine Einstellungen, die deine App beim Start oder während der Nutzung wieder lesen kann.
Das mentale Modell ist: DataStore ist eine kleine, beobachtbare Einstellungsdatei mit einer Coroutine-freundlichen API. Du fragst nicht einmalig synchron nach einem Wert und hoffst, dass er aktuell ist. Du abonnierst einen Datenstrom. Dieser Datenstrom ist ein Flow, der dir den aktuellen Zustand liefert und bei Änderungen neue Werte ausgibt. Dadurch passt Preferences DataStore gut zu modernem Android mit ViewModel, StateFlow und Jetpack Compose.
Im Android-Kontext ersetzt Preferences DataStore in vielen Fällen ältere SharedPreferences-Nutzung. Der wichtigste Unterschied für Lernende ist die Arbeitsweise: DataStore ist asynchron und auf konsistente Updates ausgelegt. Du solltest ihn deshalb nicht wie eine globale Map behandeln, auf die überall direkt zugegriffen wird. In einer sauber aufgebauten App gehört DataStore in die Data Layer, meistens in eine kleine Repository- oder Settings-Klasse. ViewModels lesen daraus UI-Zustand und schreiben neue Einstellungen über klare Methoden.
Die Grenze ist ebenfalls wichtig. Preferences DataStore ist nicht für große Listen, komplexe Objekte, Suchfunktionen oder Offline-First-Domänenmodelle gedacht. Wenn du Nutzerprofile, Aufgaben, Nachrichten oder Produkte lokal speichern willst, brauchst du eher Room oder eine andere strukturierte Lösung. Preferences DataStore ist für einfache Einstellungen da. Diese klare Einordnung verhindert, dass ein nützliches Werkzeug zu breit eingesetzt wird.
Wie funktioniert es?
Preferences DataStore arbeitet mit typisierten Preference Keys. Für jeden gespeicherten Wert definierst du einen Schlüssel, zum Beispiel booleanPreferencesKey("show_completed") oder stringPreferencesKey("sort_order"). Beim Lesen bekommst du ein Flow<Preferences>. Aus diesem Flow mapst du deine fachlich sinnvollen Werte heraus. Aus einem rohen Boolean wird dann zum Beispiel ein UserSettings-Objekt, das deine UI versteht.
Beim Schreiben verwendest du edit. Innerhalb dieses Blocks setzt du Werte über die definierten Keys. DataStore kümmert sich darum, dass diese Änderung persistiert wird und danach über den Flow sichtbar ist. Weil das Schreiben eine suspendierende Operation ist, rufst du es aus einer Coroutine auf, typischerweise aus dem ViewModel oder aus einer Use-Case-Schicht, nicht direkt aus beliebigen UI-Helfern.
Der wichtige Ablauf sieht so aus:
- Du definierst einen DataStore für den
Context. - Du kapselst Keys und Lese-/Schreiblogik in einer Klasse.
- Du wandelst
dataStore.datamit Flow-Operatoren in ein App-spezifisches Modell um. - Du stellst dieses Modell dem ViewModel bereit.
- Compose sammelt den UI-Zustand, zum Beispiel über
collectAsStateWithLifecycle.
Diese Reihenfolge ist kein Selbstzweck. Sie hält Android-Abhängigkeiten kontrolliert, macht Tests einfacher und verhindert, dass deine Composables Speicherlogik enthalten. Eine Composable soll beschreiben, wie ein Zustand aussieht und welche Events ausgelöst werden. Sie soll nicht wissen, wie Preferences.Key<Boolean> heißt.
Bei Flow ist außerdem wichtig: Ein Flow beschreibt einen Strom von Werten über Zeit. DataStore passt dazu, weil Einstellungen nicht nur beim App-Start relevant sind. Wenn der Nutzer in einem Einstellungsbildschirm einen Schalter ändert, kann eine andere Oberfläche sofort darauf reagieren. In Compose ist das sehr angenehm, weil ein neuer Wert automatisch zu neuem UI-State werden kann.
Coroutines geben dir dabei die passende Ausführungsform. Du blockierst nicht den Main Thread, während eine Datei gelesen oder geschrieben wird. Gerade auf Android ist das kein Detail, sondern Qualitätsarbeit. Blockierende I/O-Zugriffe können Ruckler auslösen, ANR-Risiken erhöhen und Tests schwerer verständlich machen. DataStore zwingt dich sanft in ein asynchrones Modell, das besser zur Plattform passt.
In der Praxis
Stell dir vor, deine App zeigt eine Aufgabenliste. Der Nutzer kann entscheiden, ob erledigte Aufgaben sichtbar bleiben sollen. Das ist eine klassische Einstellung: ein einzelner Boolean, klein, dauerhaft, beobachtbar. Dafür eignet sich Preferences DataStore sehr gut.
Eine typische Implementierung könnte so aussehen:
import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
private val Context.settingsDataStore by preferencesDataStore(name = "settings")
data class TaskSettings(
val showCompletedTasks: Boolean
)
class SettingsRepository(
private val context: Context
) {
private object Keys {
val ShowCompletedTasks = booleanPreferencesKey("show_completed_tasks")
}
val settings: Flow<TaskSettings> =
context.settingsDataStore.data.map { preferences ->
TaskSettings(
showCompletedTasks = preferences[Keys.ShowCompletedTasks] ?: true
)
}
suspend fun setShowCompletedTasks(enabled: Boolean) {
context.settingsDataStore.edit { preferences ->
preferences[Keys.ShowCompletedTasks] = enabled
}
}
}
Im ViewModel würdest du diesen Flow nicht als rohen DataStore-Flow bis in die UI durchreichen müssen. Häufig wandelst du ihn in einen StateFlow um, damit Compose einen stabilen UI-Zustand sammeln kann. Der genaue Aufbau hängt von deiner Architektur ab, aber die Richtung bleibt gleich: Repository liefert beobachtbare Daten, ViewModel formt UI-State, Composable zeigt ihn an und sendet Nutzerereignisse zurück.
Ein vereinfachtes ViewModel-Beispiel:
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.launch
class TaskSettingsViewModel(
private val settingsRepository: SettingsRepository
) : ViewModel() {
val settings = settingsRepository.settings
fun onShowCompletedTasksChanged(enabled: Boolean) {
viewModelScope.launch {
settingsRepository.setShowCompletedTasks(enabled)
}
}
}
Und in Compose denkst du in Zuständen und Events:
@Composable
fun TaskSettingsScreen(
settings: TaskSettings,
onShowCompletedTasksChanged: (Boolean) -> Unit
) {
Row {
Text(text = "Erledigte Aufgaben anzeigen")
Switch(
checked = settings.showCompletedTasks,
onCheckedChange = onShowCompletedTasksChanged
)
}
}
Das Beispiel zeigt eine wichtige Entscheidungsregel: Verwende Preferences DataStore, wenn der Wert klein, unabhängig und als Einstellung verständlich ist. Gute Kandidaten sind Booleans, Strings, Zahlen und kleine Enum-Werte, die du als String oder Int abbildest. Schlechte Kandidaten sind wachsende Sammlungen, verschachtelte Modelle, Cache-Daten von Netzwerkantworten oder Daten, die du abfragen, sortieren oder migrieren musst wie echte Entitäten.
Eine häufige Stolperfalle ist, DataStore direkt in Composables zu verwenden. Das funktioniert vielleicht in einem kleinen Experiment, führt aber schnell zu schwer testbarem Code. Deine UI hängt dann an Android-Storage-Details, und du verteilst Key-Namen über mehrere Dateien. Besser ist eine zentrale Klasse, die ein fachliches Modell liefert. So kann ein Code-Review sofort prüfen: Sind Defaults sinnvoll? Gibt es eine klare Schreibmethode? Bleibt die UI frei von Persistenzdetails?
Eine zweite Stolperfalle sind falsche Defaults. Wenn du beim Lesen ?: false verwendest, entscheidest du damit über das Verhalten neuer Installationen und über Fälle, in denen ein Wert noch nicht existiert. Dieser Default ist Produktlogik, kein technisches Detail. Er sollte bewusst gewählt und in Tests abgesichert werden. Beim Beispiel oben bedeutet ?: true, dass erledigte Aufgaben standardmäßig sichtbar sind. Das kann richtig oder falsch sein, aber es muss eine bewusste Entscheidung sein.
Eine dritte Stolperfalle ist blockierendes Denken. Du solltest nicht versuchen, DataStore synchron auszulesen, nur weil du an einer Stelle sofort einen Wert brauchst. Wenn ein Feature vom Setting abhängt, modellierst du diesen Bedarf als Flow oder als State im ViewModel. Für einmalige Aktionen kannst du Flow-Werte gezielt in einer Coroutine sammeln, aber der Standardfall bleibt beobachtbar und asynchron. Das passt auch zu Offline-First-Denken: Lokale Daten sind eine Quelle von Wahrheit, die sich ändern kann und über Streams sauber an andere Schichten weitergegeben wird.
Zum Testen kannst du klein anfangen. Prüfe zuerst die Mapping-Logik: Wenn kein Wert gespeichert ist, liefert dein Repository den erwarteten Default. Wenn du setShowCompletedTasks(false) aufrufst, erscheint danach false im Flow. In echten Projekten kapselst du DataStore so, dass du im Unit-Test entweder eine Testinstanz oder ein Fake-Repository verwenden kannst. Für ViewModel-Tests ist entscheidend, dass du nicht Android-Dateizugriffe testen musst, sondern Reaktionen auf Settings-Zustände.
Beim Debuggen hilft dir eine einfache Frage: Wo entsteht der fachliche Zustand? Wenn du in der UI noch mit Preference Keys arbeitest, ist die Schichtgrenze zu schwach. Wenn dein ViewModel dagegen nur TaskSettings(showCompletedTasks = false) kennt, ist die Persistenz sauber versteckt. Genau diese Trennung unterscheidet eine Demo von Code, der in einer wachsenden App wartbar bleibt.
Fazit
Preferences DataStore ist dein Werkzeug für einfache, dauerhafte Einstellungen als Key-Value-Datenstrom. Du speicherst keine komplexen Modelle, sondern kleine Entscheidungen der App, liest sie über Flow und leitest daraus UI-State ab. Übe das mit einem echten Setting in einer kleinen App: Definiere einen Key, mappe ihn in ein fachliches Datenmodell, schreibe über eine suspendierende Methode und prüfe im Debugger oder Test, ob Default, Änderung und UI-Reaktion korrekt zusammenspielen.