Android Coden
Android 9 min lesen

DataStore im Überblick

DataStore speichert kleine App-Einstellungen asynchron. Du lernst, wann Preferences oder Proto passen.

Viele Android-Apps müssen kleine lokale Einstellungen speichern: ein Dark-Mode-Schalter, die zuletzt gewählte Sortierung, ein Hinweis, der nur einmal gezeigt werden soll, oder ein einfacher Onboarding-Status. Früher wurde dafür oft SharedPreferences genutzt. In modernem Android ist DataStore die bessere Standardwahl für solche Daten, weil es asynchron arbeitet, gut zu Kotlin Coroutines und Flow passt und sich sauber in eine Data-Layer-Architektur einordnen lässt.

Was ist das?

DataStore ist eine Jetpack-Bibliothek zum Speichern kleiner Datenmengen auf dem Gerät. Du nutzt sie typischerweise für App-Einstellungen und einfache Zustände, die lokal erhalten bleiben sollen. Der wichtigste Gedanke: DataStore ist kein Ersatz für eine Datenbank wie Room. Es ist auch kein Cache für große API-Antworten. Es ist ein kleiner, robuster Speicher für Konfiguration und Präferenzen.

Im Android-Kontext löst DataStore ein konkretes Problem: Einstellungen sollen zuverlässig gespeichert werden, ohne den Hauptthread zu blockieren. Gerade Einsteiger unterschätzen oft, dass auch kleine Dateioperationen schlecht platziert werden können. Wenn du beim Start einer App synchron aus einer Datei liest, kann die UI ruckeln oder verzögert reagieren. DataStore vermeidet dieses Muster, indem Lese- und Schreibzugriffe über Coroutines und Flow laufen.

Es gibt zwei Varianten. Preferences DataStore speichert Schlüssel-Wert-Paare. Das passt, wenn du wenige einfache Werte hast, etwa Boolean, String oder Int. Du definierst dabei Keys im Code, aber kein festes Schema für die komplette Datei. Proto DataStore speichert dagegen ein typisiertes Objekt, das über Protocol Buffers beschrieben wird. Das ist sinnvoll, wenn deine Einstellungen stärker zusammengehören und du ein klares Modell möchtest, zum Beispiel ein UserSettings-Objekt mit mehreren Feldern.

Für deinen Lernweg ist das mentale Modell wichtiger als jede einzelne API: DataStore ist eine kleine lokale Datenquelle in deiner Data Layer. Deine UI fragt nicht direkt danach, ob eine Datei existiert. Stattdessen stellt ein Repository oder eine Settings-Quelle einen Flow bereit. Die UI beobachtet diesen Flow und reagiert auf Änderungen. Schreibvorgänge laufen über suspendierende Funktionen. So bleibt dein Code testbarer und näher an den üblichen Architektur-Empfehlungen für Android.

Der Roadmap-Kontext ist bewusst eng: Verwende DataStore für kleine Einstellungen statt alter SharedPreferences. Sobald du Listen, Relationen, Suchabfragen, Paging, Konfliktauflösung oder größere Offline-Daten brauchst, bist du bei anderen Bausteinen wie Room, Netzwerk-Repositories und einer umfassenderen Offline-First-Strategie. DataStore kann Teil einer Offline-First-App sein, aber es trägt dort meist Einstellungen, Feature-Flags, lokale Nutzerpräferenzen oder Sync-Metadaten, nicht den gesamten Datenbestand.

Wie funktioniert es?

DataStore speichert Daten intern in einer Datei und stellt dir eine asynchrone API bereit. Beim Lesen bekommst du keinen Wert direkt zurück, sondern einen Flow. Dieser Flow sendet den aktuellen Zustand und später neue Zustände, wenn sich die gespeicherten Daten ändern. Das passt gut zu Compose, weil Compose ohnehin zustandsbasiert arbeitet: Daten ändern sich, die UI wird neu berechnet.

Bei Preferences DataStore definierst du Keys. Ein Key ist der Name eines gespeicherten Werts plus sein Typ. Beim Lesen mapst du die gespeicherten Preferences auf dein eigenes Domänen- oder UI-Modell. Beim Schreiben verwendest du edit, änderst innerhalb dieses Blocks die gewünschten Keys und DataStore kümmert sich um die persistente Speicherung.

Wichtig ist: Der Zugriff ist nicht synchron. Du solltest also nicht versuchen, DataStore wie eine Map zu behandeln, aus der du jederzeit sofort einen Wert ziehst. Das führt zu schlechten Mustern wie runBlocking im UI-Code oder zu unnötigen Startblockaden. Stattdessen lässt du den Flow durch deine Architektur laufen. Ein Repository kann zum Beispiel einen Flow<ThemeMode> bereitstellen, den dein ViewModel in einen StateFlow umwandelt. Compose sammelt diesen Zustand dann mit einer lifecycle-bewussten API ein.

DataStore passt gut zu Coroutine-Best-Practices, weil lange oder potenziell blockierende Arbeit nicht in der UI-Schicht landen sollte. Die UI löst Absichten aus: Der Nutzer aktiviert einen Schalter, wählt eine Sprache oder ändert eine Sortierung. Das ViewModel ruft eine suspendierende Repository-Funktion auf. Das Repository schreibt in DataStore. Danach sendet der Flow einen neuen Zustand, den die UI wieder anzeigt. Dadurch brauchst du keine manuelle Aktualisierung an zwei Stellen.

Proto DataStore funktioniert vom Prinzip her ähnlich, aber mit einem festen Schema. Du definierst in einer .proto-Datei, welche Felder dein gespeichertes Objekt besitzt. Daraus wird Code generiert. Beim Lesen bekommst du ein typisiertes Objekt, beim Schreiben aktualisierst du dieses Objekt. Das reduziert Fehler durch falsche Keys oder uneinheitliche Namen. Dafür ist der Einstieg etwas aufwendiger, weil du Protocol Buffers und die Serialisierung einrichten musst.

Die Entscheidung zwischen Preferences und Proto ist eine typische Praxisfrage. Verwende Preferences DataStore, wenn du wenige unabhängige Einstellungen speicherst und die Struktur überschaubar bleibt. Verwende Proto DataStore, wenn du ein zusammenhängendes Settings-Modell hast, Typ-Sicherheit wichtig ist oder du vermeiden willst, dass viele String-Keys über das Projekt verteilt werden. Wenn du später viele Felder ergänzt, ist Proto oft sauberer. Für einen einzelnen Dark-Mode-Schalter wäre Proto aber meist zu schwer.

Auch der Lebenszyklus ist relevant. DataStore sollte nicht bei jeder Funktion neu erzeugt werden. Üblich ist eine einzige Instanz pro Datei, oft über eine Property auf Context-Ebene oder über Dependency Injection. Wenn du mehrere Instanzen für dieselbe Datei erzeugst, riskierst du unnötige Komplexität und schwer nachvollziehbares Verhalten. In einer App mit sauberer Data Layer liegt DataStore hinter einer Klasse wie SettingsDataSource oder UserPreferencesRepository.

Im Alltag begegnet dir DataStore oft an Schnittstellen zwischen App-Start, Einstellungen und UI-Zustand. Beispiel: Deine App soll direkt im zuletzt gewählten Theme starten. Das heißt aber nicht, dass du den Hauptthread blockieren solltest, bis die Einstellung geladen ist. Besser ist ein Startzustand, etwa System, und anschließend ein Flow, der den gespeicherten Wert liefert. Die UI kann daraus stabil einen Theme-Zustand ableiten.

Für Offline-First-Apps ist DataStore ein kleiner, aber nützlicher Baustein. Du kannst zum Beispiel speichern, wann zuletzt synchronisiert wurde, ob nur über WLAN synchronisiert werden soll oder welche Ansicht der Nutzer bevorzugt. Die eigentlichen Offline-Daten gehören jedoch in eine dafür geeignete lokale Quelle. DataStore bleibt bei kleinen, klaren Einstellungen. Diese Grenze schützt dich davor, später gegen das Werkzeug zu arbeiten.

In der Praxis

Ein typischer Einstieg ist ein Repository für App-Einstellungen. Die UI kennt dann nicht die DataStore-Keys, sondern nur fachliche Funktionen und Flows. Das folgende Beispiel zeigt Preferences DataStore für einen Dark-Mode-Schalter. Es ist bewusst klein gehalten, damit du die Struktur erkennst.

private val Context.settingsDataStore by preferencesDataStore(
    name = "settings"
)

class SettingsRepository(
    private val dataStore: DataStore<Preferences>
) {
    private object Keys {
        val darkModeEnabled = booleanPreferencesKey("dark_mode_enabled")
    }

    val darkModeEnabled: Flow<Boolean> =
        dataStore.data
            .map { preferences ->
                preferences[Keys.darkModeEnabled] ?: false
            }

    suspend fun setDarkModeEnabled(enabled: Boolean) {
        dataStore.edit { preferences ->
            preferences[Keys.darkModeEnabled] = enabled
        }
    }
}

In einer echten App würdest du SettingsRepository über Dependency Injection bereitstellen. Dein ViewModel kann den Flow dann in einen StateFlow umwandeln und Schreibvorgänge in viewModelScope ausführen.

class SettingsViewModel(
    private val repository: SettingsRepository
) : ViewModel() {
    val darkModeEnabled: StateFlow<Boolean> =
        repository.darkModeEnabled
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = false
            )

    fun onDarkModeChanged(enabled: Boolean) {
        viewModelScope.launch {
            repository.setDarkModeEnabled(enabled)
        }
    }
}

In Compose bleibt die Komponente dadurch schlank. Sie zeigt Zustand an und meldet Nutzeraktionen zurück. Sie entscheidet nicht, wie Daten gespeichert werden.

@Composable
fun SettingsScreen(
    viewModel: SettingsViewModel
) {
    val darkModeEnabled by viewModel.darkModeEnabled.collectAsStateWithLifecycle()

    Row(
        verticalAlignment = Alignment.CenterVertically
    ) {
        Text(text = "Dunkles Design")
        Switch(
            checked = darkModeEnabled,
            onCheckedChange = viewModel::onDarkModeChanged
        )
    }
}

Die wichtigste Entscheidungsregel lautet: Speichere in DataStore nur kleine, stabile Einstellungen, die als einzelne Werte oder als kleines Settings-Modell sinnvoll sind. Wenn du mehrere Entitäten, Listen, Filter, Suchabfragen oder Beziehungen modellierst, nimm Room oder eine andere passende Datenquelle. Wenn ein Wert vom Server kommt und nur temporär beschleunigt werden soll, prüfe, ob er wirklich eine Einstellung ist oder eher Cache-Daten darstellt.

Eine häufige Stolperfalle ist der Versuch, DataStore synchron zu lesen. Manche Entwickler möchten beim App-Start sofort den gespeicherten Wert haben und greifen zu runBlocking. Das kann die Startzeit verschlechtern und macht den Code schwerer testbar. Besser ist es, mit einem Anfangswert zu arbeiten und die UI auf den Flow reagieren zu lassen. Wenn ein Wert wirklich vor dem ersten Screen bekannt sein muss, solltest du diese Anforderung bewusst architektonisch behandeln und nicht zufällig im UI-Code blockieren.

Eine zweite Stolperfalle ist die Vermischung von DataStore-Zugriff und Compose-Code. Es wirkt verlockend, direkt in einer Composable auf context.dataStore.data zuzugreifen. Technisch kann man viel verdrahten, aber die Verantwortung liegt dann an der falschen Stelle. Composables sollten UI beschreiben. Datenzugriff gehört in Repository, Data Source oder ViewModel. Das macht Tests einfacher und verhindert, dass du Speicherlogik über mehrere Screens verteilst.

Auch Fehlerbehandlung gehört zum Praxisbild. Beim Lesen aus einer Datei können Ausnahmen auftreten. In produktivem Code solltest du überlegen, wie du mit IOException oder beschädigten Daten umgehst. Für Preferences DataStore ist oft ein sinnvoller Default-Wert ausreichend. Bei Proto DataStore spielen Serializer und Default-Instanzen eine größere Rolle. Die Grundidee bleibt: Die UI sollte einen stabilen Zustand bekommen, auch wenn die gespeicherte Datei nicht perfekt ist.

Beim Testen kannst du dein Verständnis gut prüfen. Schreibe einen kleinen Test für dein Repository: Starte mit leerem DataStore, prüfe den Default-Wert, schreibe einen neuen Wert und prüfe, ob der Flow den neuen Zustand liefert. In einem Code-Review solltest du besonders auf drei Fragen achten: Liegt DataStore hinter einer klaren Daten-Schicht? Wird kein Hauptthread blockiert? Sind die gespeicherten Werte wirklich Einstellungen und keine verkappte Datenbank?

Wenn du bereits SharedPreferences in einem älteren Projekt hast, solltest du nicht nur mechanisch ersetzen. Prüfe zuerst, welche Werte dort liegen. Manche Werte sind echte Einstellungen und passen gut zu DataStore. Andere sind vielleicht veraltete Caches, temporäre Flags oder Daten, die in eine andere Schicht gehören. Eine Migration ist eine gute Gelegenheit, die Bedeutung der gespeicherten Werte zu klären.

Fazit

DataStore ist der moderne Standard für kleine lokale Einstellungen in Android-Apps. Du solltest es als asynchrone Datenquelle in deiner Data Layer verstehen: Preferences für einfache Schlüssel-Wert-Einstellungen, Proto für ein typisiertes Settings-Modell. Übe das Thema nicht nur durch Lesen, sondern baue einen kleinen Settings-Screen, beobachte den Flow im Debugger und schreibe einen Repository-Test für Lesen und Schreiben. Danach erkennst du in Code-Reviews schneller, ob DataStore sinnvoll eingesetzt wird oder ob gerade eine Datenbank, ein Cache oder UI-Code zweckentfremdet wird.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

Das Redaktionsteam recherchiert und schreibt Artikel zu aktuellen Themen rund um Tech, Lifestyle und Ratgeber.