Data Layer: Verantwortlichkeiten im Überblick
Der Data Layer trennt Datenzugriff und Caching sauber vom UI-Code. Lerne, was Repositories und Data Sources leisten.
Wer eine Android-App entwickelt, stößt früh auf eine grundlegende Frage: Wo laden, speichere und aktualisiere ich Daten – und wer ist dafür verantwortlich? Ohne eine klare Antwort wandert Datenbankcode ins ViewModel, Netzwerkaufrufe landen direkt in Composables, und Tests werden nahezu unmöglich. Der Data Layer ist die Antwort der offiziellen Android-Architektur auf genau dieses Problem.
Was ist das?
Der Data Layer ist die Schicht in der Android-App-Architektur, die ausschließlich für den Umgang mit Daten zuständig ist. Er sitzt unterhalb der Domain Layer (oder direkt unterhalb der UI-Schicht, wenn keine Domain Layer vorhanden ist) und enthält zwei Arten von Klassen:
Repositories fassen alle Datenzugriffe zu einer kohärenten API zusammen. Sie sind der einzige Anlaufpunkt für ViewModels, Use Cases oder andere Konsumenten. Das Repository entscheidet intern, ob Daten aus einem lokalen Cache, einer Datenbank oder dem Netzwerk geliefert werden.
Data Sources implementieren den tatsächlichen Zugriff auf eine konkrete Quelle – eine lokale Datenbank (Room), einen Schlüssel-Wert-Speicher (DataStore), eine REST-API (Retrofit) oder einen Cloud-Dienst (Firebase). Eine Data Source kennt keine andere Data Source; sie löst genau eine Aufgabe.
Die klare Aufgabenteilung bedeutet: UI-Code fragt ein Repository nach dem, was er braucht – er weiß nicht und soll nicht wissen, woher die Daten stammen oder ob sie gerade frisch aus dem Netzwerk kommen.
Wie funktioniert es?
Ein Repository hält Referenzen auf eine oder mehrere Data Sources und koordiniert deren Zusammenspiel. Der typische Datenfluss läuft so ab:
- Das ViewModel ruft eine Methode des Repositories auf, z. B.
getUserProfile(id). - Das Repository prüft, ob ein gültiger Eintrag in der lokalen Datenbank vorhanden ist.
- Falls ja, liefert es den lokalen Datensatz zurück und startet im Hintergrund ggf. eine Aktualisierung.
- Falls nein, holt es die Daten von der Remote Data Source, speichert sie lokal und reicht sie weiter.
Die Daten fließen in der Regel als Flow<T>, damit das ViewModel reaktiv auf Änderungen reagiert, ohne selbst Polling betreiben zu müssen.
Ein typisches Interface-Paar sieht so aus:
interface UserRemoteDataSource {
suspend fun fetchUser(id: String): UserDto
}
interface UserRepository {
fun getUserStream(id: String): Flow<User>
suspend fun refreshUser(id: String)
}
Die Implementierung des Repositories kombiniert beide Seiten:
class UserRepositoryImpl(
private val remoteSource: UserRemoteDataSource,
private val localSource: UserLocalDataSource
) : UserRepository {
override fun getUserStream(id: String): Flow<User> =
localSource.observeUser(id)
override suspend fun refreshUser(id: String) {
val dto = remoteSource.fetchUser(id)
localSource.upsertUser(dto.toEntity())
}
}
Das ViewModel ruft nur getUserStream auf und beobachtet den Flow. Wo die Daten herkommen und wie sie synchronisiert werden, bleibt vollständig verborgen.
In der Praxis
Caching gehört ins Repository, nicht ins ViewModel
Eine häufige Stolperfalle ist, Cache-Logik direkt im ViewModel zu implementieren – etwa eine Map<String, User> als In-Memory-Cache. Das Problem: Beim Neuerstellen des ViewModels (z. B. nach einer Konfigurationsänderung) geht der Cache verloren. Noch schlimmer: Zwei verschiedene ViewModels haben keinen gemeinsamen Cache und machen redundante Netzwerkanfragen.
Korrekt ist: Room übernimmt den persistenten Cache, das Repository koordiniert, wann Daten erneut vom Server geladen werden müssen. So sehen alle Konsumenten denselben Stand, und der Cache überlebt Konfigurationsänderungen.
Offline-First als Standardstrategie
Für Apps, die auch ohne Internetverbindung nutzbar sein sollen, empfiehlt die offizielle Dokumentation das Prinzip „write to local, sync to remote”: Der Nutzer sieht immer die lokalen Daten, eine Hintergrundlogik synchronisiert asynchron mit dem Server. Wenn die Verbindung unterbrochen ist, landen Schreiboperationen in einer lokalen Warteschlange (z. B. WorkManager-Jobs), die nach Wiederherstellung der Verbindung abgearbeitet wird. Das Repository implementiert diese Logik vollständig; das ViewModel bleibt davon unberührt.
Typische Stolperfalle: Geschäftslogik im Repository
Repositories sollen Daten beschaffen und speichern, keine komplexen Berechnungen anstellen. Wenn du dich dabei ertappst, im Repository mehrere Datensätze zusammenzuführen und dabei Geschäftsregeln anzuwenden, ist ein Use Case in der Domain Layer der richtige Ort dafür. Die Faustregel lautet: Enthält eine Methode des Repositories ein if basierend auf Geschäftsregeln statt auf Datenverfügbarkeit, gehört diese Logik eine Schicht höher.
WorkManager koppelst du ans Repository, nie ans ViewModel
Wenn regelmäßige Hintergrundsynchronisation nötig ist – etwa stündliche Aktualisierung von Kursdaten –, koppelst du den WorkManager-Worker direkt mit dem Repository. Das ViewModel lebt nur so lange, wie der zugehörige Screen sichtbar ist, und eignet sich daher nicht als Koordinationspunkt für Hintergrundarbeit.
Fazit
Der Data Layer ist das Herzstück einer wartbaren Android-App: Er trennt Datenzugriff, Caching und Synchronisation sauber vom UI-Code und gibt Repositories eine klar definierte Rolle als einzige Schnittstelle nach oben. Wenn du das nächste Mal eine Funktion implementierst, die Daten lädt oder speichert, frag dich konsequent: Ist das Repository- oder Data-Source-Logik? Schreib anschließend einen Unit-Test für das Repository, der die Remote Data Source mockt, und prüfe explizit, ob das Caching korrekt greift, wenn der Server nicht erreichbar ist – dieser Test deckt die häufigsten Fehler im Data Layer zuverlässig auf.