Proto DataStore
Proto DataStore speichert strukturierte App-Einstellungen typsicher. Du lernst, wann Schema, Protobuf und Evolution wichtig werden.
Proto DataStore ist die strukturierte Variante von Jetpack DataStore. Du verwendest ihn, wenn App-Einstellungen nicht nur aus losen Schlüssel-Wert-Paaren bestehen, sondern ein klares, typisiertes Modell brauchen: zum Beispiel mehrere zusammengehörige Preferences, Zustände für Offline-Sync oder Konfigurationen, die über App-Versionen hinweg stabil lesbar bleiben müssen.
Was ist das?
Proto DataStore ist ein lokaler Speicher für kleine, strukturierte Daten in Android-Apps. Statt Werte über String-Schlüssel zu lesen, definierst du ein Schema mit Protocol Buffers, kurz Protobuf. Aus diesem Schema wird Kotlin- beziehungsweise Java-Code generiert. Dadurch arbeitest du im App-Code mit echten Typen: Boolean, Int, String, Enums oder verschachtelten Nachrichten.
Das mentale Modell ist wichtig: Proto DataStore ist kein Ersatz für Room und keine allgemeine Datenbank. Er speichert typischerweise Einstellungen oder kleine Zustandsobjekte, die du als ein zusammenhängendes Dokument lesen und schreiben möchtest. Wenn du eine Liste vieler Entitäten, komplexe Abfragen oder Relationen brauchst, gehört das eher in eine Datenbank. Wenn du aber eine klar definierte App-Konfiguration brauchst, passt Proto DataStore sehr gut.
Im modernen Android-Kontext sitzt Proto DataStore oft in der Data Layer. Ein Repository kapselt den Zugriff, stellt einen Flow mit den aktuellen Einstellungen bereit und bietet Schreibfunktionen an. Deine UI, etwa mit Jetpack Compose, beobachtet diesen Flow über ViewModel-State. So bleibt die UI frei von Speicherlogik, und du kannst Verhalten leichter testen.
Der zentrale Vorteil gegenüber losem Speichern ist das typed schema. Dein Schema beschreibt, welche Felder existieren und welche Typen sie haben. Fehler wie ein falsch geschriebener Schlüssel oder eine unerwartete Typumwandlung wandern damit früher in die Entwicklungsphase. Das ist besonders hilfreich, wenn Einstellungen fachliche Bedeutung bekommen: etwa ob Offline-Inhalte nur über WLAN synchronisiert werden, welcher Sortiermodus aktiv ist oder welcher Nutzerbereich zuletzt geöffnet war.
Wie funktioniert es?
Bei Proto DataStore definierst du zuerst eine .proto-Datei. Darin beschreibst du eine Nachricht, zum Beispiel UserSettings. Jedes Feld bekommt einen Namen, einen Typ und eine Feldnummer. Diese Nummer ist nicht Dekoration, sondern Teil des Speicherformats. Protobuf verwendet sie, um Daten stabil zu kodieren und später wieder zu lesen.
Aus dem Schema erzeugt das Build-System Klassen. In Kotlin verwendest du dann einen Serializer, der Default-Werte bereitstellt und Daten aus einem InputStream liest oder in einen OutputStream schreibt. DataStore selbst kümmert sich um asynchronen Zugriff, atomare Updates und einen Flow, über den Änderungen beobachtbar werden.
Wichtig ist die Evolution des Schemas. Apps leben über Versionen hinweg. Nutzer haben vielleicht Version 1 installiert, speichern Daten und aktualisieren Wochen später auf Version 2. Dein neues Schema muss alte Daten weiter verstehen. Deshalb solltest du Feldnummern nie wiederverwenden, wenn ein Feld entfernt wurde. Neue Felder ergänzt du mit neuen Nummern und sinnvollen Default-Werten. Bestehende Feldbedeutungen änderst du nur sehr vorsichtig, weil gespeicherte Daten sonst semantisch falsch interpretiert werden können.
Im Alltag bedeutet das: Du behandelst die .proto-Datei wie einen Vertrag. Änderungen daran gehören ins Code-Review. Frag dich bei jeder Änderung: Können Daten aus einer älteren App-Version noch gelesen werden? Ist der Default-Wert fachlich korrekt? Muss eine Migration geschrieben werden? Diese Fragen wirken klein, verhindern aber schwer zu findende Fehler nach Releases.
DataStore passt gut zu Offline-First-Architektur, solange du die Grenze sauber ziehst. Offline-First bedeutet nicht, alle Daten in Proto DataStore zu speichern. Es bedeutet, lokale Datenquellen bewusst zu modellieren. Proto DataStore kann dabei Konfigurationen speichern, die Offline-Verhalten steuern, zum Beispiel Sync-Intervalle, Konfliktstrategien oder Nutzerentscheidungen. Die eigentlichen fachlichen Datensätze liegen meist in Room oder einer anderen lokalen Datenquelle.
In der Praxis
Ein typisches Beispiel ist eine Einstellung für Synchronisation. Du möchtest speichern, ob nur über WLAN synchronisiert werden soll und welcher Sync-Modus aktiv ist. Mit Preferences DataStore könntest du dafür Schlüssel definieren. Mit Proto DataStore beschreibst du das Modell direkt.
Protobuf-Schema
syntax = "proto3";
option java_package = "de.androidcoden.settings";
option java_multiple_files = true;
message SyncSettings {
bool wifi_only = 1;
SyncMode mode = 2;
}
enum SyncMode {
SYNC_MODE_UNSPECIFIED = 0;
SYNC_MODE_MANUAL = 1;
SYNC_MODE_AUTOMATIC = 2;
}
Achte auf SYNC_MODE_UNSPECIFIED. Bei Protobuf ist der erste Enum-Wert der Default. Ein unklarer Default wie AUTOMATIC kann später zu unerwartetem Verhalten führen, wenn alte Daten kein Feld enthalten. Ein expliziter unbekannter oder nicht gesetzter Zustand zwingt dich, im Kotlin-Code bewusst zu entscheiden.
Serializer und Repository
object SyncSettingsSerializer : Serializer<SyncSettings> {
override val defaultValue: SyncSettings = SyncSettings.newBuilder()
.setWifiOnly(true)
.setMode(SyncMode.SYNC_MODE_MANUAL)
.build()
override suspend fun readFrom(input: InputStream): SyncSettings {
return try {
SyncSettings.parseFrom(input)
} catch (exception: InvalidProtocolBufferException) {
throw CorruptionException("Sync settings cannot be read.", exception)
}
}
override suspend fun writeTo(t: SyncSettings, output: OutputStream) {
t.writeTo(output)
}
}
class SyncSettingsRepository(
private val dataStore: DataStore<SyncSettings>
) {
val settings: Flow<SyncSettings> = dataStore.data
suspend fun setWifiOnly(enabled: Boolean) {
dataStore.updateData { current ->
current.toBuilder()
.setWifiOnly(enabled)
.build()
}
}
suspend fun setMode(mode: SyncMode) {
dataStore.updateData { current ->
current.toBuilder()
.setMode(mode)
.build()
}
}
}
Dieses Repository ist absichtlich klein. Es zeigt die wichtigste Regel: Schreibe nicht direkt aus der UI in DataStore. Kapsle den Zugriff in der Data Layer. Ein ViewModel kann dann settings sammeln, in UI-State übersetzen und Schreibmethoden auslösen. In Compose beobachtest du den State, ohne wissen zu müssen, ob er aus DataStore, Room oder einer Test-Implementierung kommt.
Eine typische Stolperfalle ist das Blockieren des Main Threads. DataStore ist für coroutines und Flow gedacht. Du solltest nicht versuchen, synchron einen Wert herauszuziehen, nur weil ein Button sofort eine Entscheidung braucht. Modelliere den Zustand als Stream und leite daraus UI ab. Wenn du einen einmaligen Wert brauchst, verwende coroutine-fähige APIs wie first() in einer passenden Schicht, nicht mitten in einer Composable.
Eine zweite Stolperfalle betrifft Schema-Evolution. Angenommen, du entfernst wifi_only = 1, weil du später ein neues Feld network_policy einführst. Dann darfst du Feldnummer 1 nicht für etwas Neues verwenden. Reserviere entfernte Nummern und Namen, damit niemand sie versehentlich erneut nutzt.
message SyncSettings {
reserved 1;
reserved "wifi_only";
SyncMode mode = 2;
NetworkPolicy network_policy = 3;
}
Eine gute Entscheidungsregel lautet: Verwende Proto DataStore, wenn deine Einstellung fachlich zusammengehört, typisiert sein soll und über Releases stabil bleiben muss. Verwende Preferences DataStore, wenn du nur wenige unabhängige primitive Werte ohne starkes Schema speicherst. Verwende Room, wenn du viele Datensätze, Abfragen, Sortierung oder Relationen brauchst.
Zum Prüfen deines Verständnisses kannst du einen kleinen Test schreiben: Erzeuge ein altes Protobuf-Objekt, speichere es über den Serializer und lies es mit dem neuen Schema wieder ein. Prüfe, ob Default-Werte stimmen und keine Ausnahme entsteht. Im Code-Review solltest du jede .proto-Änderung wie eine öffentliche Schnittstelle behandeln, auch wenn sie nur lokal in der App liegt.
Fazit
Proto DataStore gibt dir für strukturierte Einstellungen einen klaren Vertrag: Das Protobuf-Schema beschreibt die Daten, Kotlin nutzt generierte Typen, und DataStore liefert asynchrone, beobachtbare Updates. Der wichtigste Lernschritt ist nicht die einzelne API, sondern der Umgang mit Schema-Evolution: Ergänze Felder kompatibel, verwende sinnvolle Default-Werte und ändere bestehende Bedeutungen nur mit Plan. Prüfe das aktiv in einer kleinen Beispiel-App, im Debugger, mit Serializer-Tests und im Code-Review deiner .proto-Dateien.