Android Coden
Android 8 min lesen

Data Layer Overview: Datenfluss in Android verstehen

Du lernst, wie Daten von Quellen über Repositories bis zur Compose-UI fließen. Der Fokus liegt auf klaren Modellen.

Wenn du eine Android-App baust, ist die UI nur der sichtbare Teil. Dahinter muss geklärt sein, wo Daten herkommen, wie sie gespeichert werden, wann sie aktualisiert werden und in welcher Form sie bei Compose ankommen. Genau darum geht es bei der Data Layer: Sie ordnet den Weg von Netzwerk oder lokaler Speicherung bis zu stabilem UI-State, ohne dass deine Screens jedes technische Detail kennen müssen.

Was ist das?

Die Data Layer ist die Schicht deiner App, die Daten beschafft, speichert, aktualisiert und in eine Form bringt, mit der der Rest der Anwendung arbeiten kann. Sie sitzt typischerweise unter der Domain- oder UI-nahen Logik und kapselt konkrete Datenquellen. Solche Quellen können eine REST-API, eine lokale Room-Datenbank, DataStore, ein In-Memory-Cache oder ein SDK sein.

Das zentrale mentale Modell ist: Deine UI fragt nicht direkt die API und schreibt nicht direkt in die Datenbank. Stattdessen spricht sie über ViewModel und eventuell Use Cases mit einem Repository. Das Repository entscheidet, welche Quelle genutzt wird, wie Daten kombiniert werden und welches Modell nach außen gegeben wird. Dadurch bleibt die Oberfläche stabil, selbst wenn sich die technische Herkunft der Daten ändert.

In modernen Android-Apps mit Kotlin, Jetpack und Compose ist diese Trennung besonders wichtig, weil Daten häufig asynchron fließen. Eine Liste kann erst aus dem Cache kommen, dann aus der Datenbank, später aus dem Netzwerk aktualisiert werden und danach automatisch neu in der UI erscheinen. Coroutines helfen dir bei einzelnen asynchronen Operationen, Flow bei fortlaufenden Datenströmen. Compose beobachtet am Ende State und zeichnet die Oberfläche neu, wenn sich dieser State ändert.

Die drei Begriffe aus diesem Thema gehören eng zusammen. Sources sind die konkreten Datenquellen. Repositories sind die vermittelnde Schnittstelle, die Quellen koordiniert. Models beschreiben die Form der Daten auf verschiedenen Ebenen. Ein sauberer Data-Layer-Entwurf bedeutet nicht, möglichst viele Klassen zu erzeugen. Er bedeutet, dass Verantwortlichkeiten klar sind und Änderungen an einer Stelle nicht unnötig viele andere Stellen beschädigen.

Wie funktioniert es?

Ein typischer Datenfluss beginnt bei einer Quelle. Eine RemoteDataSource ruft zum Beispiel eine HTTP-Schnittstelle auf. Eine LocalDataSource liest und schreibt Daten in Room. Beide kennen technische Details: Endpunkte, DAO-Methoden, DTOs, Entities, Fehler von Netzwerk oder Datenbank. Diese Details sollten nicht ungefiltert bis zur UI wandern.

Das Repository bildet die fachliche API deiner Data Layer. Es kann Methoden wie observeArticles(), refreshArticles() oder getUserProfile() anbieten. Nach außen sollte der Name ausdrücken, was die App fachlich braucht, nicht wie die Daten technisch geholt werden. Intern kann das Repository entscheiden: Zuerst lokale Daten ausgeben, dann remote aktualisieren, bei Erfolg lokal speichern, bei Fehler eine passende Fehlerform liefern.

Für fortlaufende Daten eignet sich Flow. Wenn deine Room-Abfrage einen Flow liefert, kann jede Änderung in der lokalen Datenbank automatisch weitergereicht werden. Das ViewModel kann diesen Flow sammeln, in UI-State umformen und Compose stellt diesen State dar. Für einzelne Aktionen, etwa das Auslösen eines Refreshs, nutzt du oft eine suspend-Funktion. Wichtig ist, dass lange Arbeit nicht auf dem Main Thread läuft und dass Coroutine-Scopes passend zum Lebenszyklus gewählt werden. In der Regel startet das ViewModel App-nahe Arbeit in viewModelScope, während das Repository keine UI-Lebenszyklen kennen sollte.

Modelle verdienen besondere Aufmerksamkeit. Ein API-DTO ist nicht automatisch ein gutes UI-Modell. DTOs spiegeln die externe Schnittstelle. Entities spiegeln lokale Speicherung. Domain-Modelle beschreiben, womit deine App fachlich arbeitet. UI-State beschreibt, was ein Screen zum Zeichnen braucht: Daten, Ladezustand, Fehlerhinweise, leere Zustände. Bei kleinen Apps können manche Modelle gleich aussehen oder sogar bewusst zusammenfallen. Trotzdem solltest du die Rollen gedanklich trennen. So erkennst du früher, wann eine Vermischung gefährlich wird.

Ein häufiger Fehler ist, im ViewModel direkt Retrofit-Services, DAOs oder JSON-Strukturen zu verwenden. Das wirkt am Anfang kurz, führt aber schnell zu enger Kopplung. Wenn später Offline-First, Caching, Tests oder andere Datenquellen dazukommen, musst du UI-nahe Klassen anfassen. Besser ist eine klare Regel: UI und ViewModel kennen keine technischen Datenquellen. Sie kennen Repository-Schnittstellen und die Modelle, die sie wirklich brauchen.

Eine zweite Stolperfalle ist ein unklarer Umgang mit Fehlern und Ladezuständen. Wenn ein Flow nur eine Liste liefert, fehlt der UI die Information, ob gerade geladen wird, ob alte Daten angezeigt werden oder ob ein Fehler passiert ist. Das kann bewusst okay sein, etwa bei stabilen lokalen Daten. Für echte Nutzeroberflächen brauchst du aber oft eine Ergebnisform, zum Beispiel ein UiState oder eine eigene Result-Struktur, damit die Oberfläche nachvollziehbar reagieren kann.

In der Praxis

Stell dir eine einfache Artikelliste vor. Die App soll Artikel anzeigen, lokal beobachten und per Refresh vom Netzwerk aktualisieren. Die UI soll nicht wissen, ob die Daten aus Room oder von einer API kommen. Sie soll nur einen Zustand erhalten, den sie darstellen kann.

Eine mögliche Struktur sieht so aus:

data class Article(
    val id: String,
    val title: String,
    val teaser: String
)

data class ArticleEntity(
    val id: String,
    val title: String,
    val teaser: String
)

data class ArticleDto(
    val id: String,
    val headline: String,
    val summary: String
)

fun ArticleEntity.toDomain() = Article(
    id = id,
    title = title,
    teaser = teaser
)

fun ArticleDto.toEntity() = ArticleEntity(
    id = id,
    title = headline,
    teaser = summary
)

interface ArticleRepository {
    fun observeArticles(): Flow<List<Article>>
    suspend fun refreshArticles()
}

class DefaultArticleRepository(
    private val local: ArticleLocalDataSource,
    private val remote: ArticleRemoteDataSource
) : ArticleRepository {

    override fun observeArticles(): Flow<List<Article>> {
        return local.observeArticles()
            .map { entities -> entities.map { it.toDomain() } }
    }

    override suspend fun refreshArticles() {
        val remoteArticles = remote.fetchArticles()
        local.replaceArticles(remoteArticles.map { it.toEntity() })
    }
}

data class ArticlesUiState(
    val isLoading: Boolean = false,
    val articles: List<Article> = emptyList(),
    val errorMessage: String? = null
)

class ArticlesViewModel(
    private val repository: ArticleRepository
) : ViewModel() {

    val uiState: StateFlow<ArticlesUiState> =
        repository.observeArticles()
            .map { articles ->
                ArticlesUiState(articles = articles)
            }
            .stateIn(
                scope = viewModelScope,
                started = SharingStarted.WhileSubscribed(5_000),
                initialValue = ArticlesUiState(isLoading = true)
            )

    fun refresh() {
        viewModelScope.launch {
            repository.refreshArticles()
        }
    }
}

An diesem Beispiel siehst du mehrere wichtige Grenzen. ArticleDto bleibt nahe an der API. Wenn der Server headline liefert, muss deine UI das nicht wissen. ArticleEntity bleibt nahe an der lokalen Speicherung. Article ist das Modell, mit dem deine App fachlich arbeitet. Das Repository verbindet Remote- und Local-Quelle und gibt einen Flow aus Domain-Modellen heraus.

In einer echten App würdest du Fehler und Ladezustände sorgfältiger behandeln. refreshArticles() könnte Exceptions abfangen oder ein Ergebnis zurückgeben. Das ViewModel könnte während eines Refreshs isLoading setzen und eine Fehlermeldung anzeigen. Trotzdem bleibt die Grundidee gleich: Die UI soll einen Zustand rendern, nicht selbst Datenquellen koordinieren.

Eine praktische Entscheidungsregel lautet: Wenn eine Klasse wissen muss, ob Daten aus Netzwerk, Datenbank oder Cache kommen, gehört dieses Wissen meist in die Data Layer, nicht in den Screen. Wenn eine Klasse wissen muss, ob ein Button deaktiviert ist oder welcher Text bei leerer Liste angezeigt wird, gehört dieses Wissen eher in den UI-State. Diese Grenze ist nicht immer perfekt, aber sie schützt dich vor vielen späteren Umbauten.

Im Alltag taucht dieses Thema bei fast jeder nichttrivialen App auf. Du baust eine Profilseite, eine Suche, einen Warenkorb, eine Favoritenliste oder einen Offline-Modus. Immer musst du entscheiden, welche Quelle führend ist, wann aktualisiert wird und welche Datenform nach außen sichtbar ist. Für Junior-Devs ist hier besonders wichtig: Suche nicht nach der einen perfekten Architektur. Suche nach klaren Abhängigkeiten. Die obere Schicht darf die untere Schicht verwenden. Die untere Schicht sollte nicht die obere kennen.

Beim Testen profitierst du direkt von dieser Trennung. Du kannst ein Repository gegen Fake-Quellen testen: Was passiert, wenn die lokale Quelle leere Daten liefert? Was passiert, wenn Remote neue Daten liefert? Wird korrekt gemappt? Wird bei einem Fehler die lokale Liste weiter beobachtet? Auch ViewModels lassen sich leichter prüfen, wenn sie nur ein Repository-Interface brauchen. Dann kannst du Flow-Werte kontrolliert senden und den entstehenden UI-State testen.

Beim Debugging hilft dir eine einfache Spur: Quelle, Repository, ViewModel, Compose-State. Prüfe zuerst, ob die Quelle korrekte Rohdaten liefert. Dann, ob das Mapping stimmt. Danach, ob das Repository den erwarteten Flow ausgibt. Anschließend, ob das ViewModel daraus passenden UI-State baut. Erst wenn diese Punkte stimmen, suchst du in der Compose-Darstellung. So vermeidest du, UI-Code zu verdächtigen, obwohl das Problem eigentlich im Mapping oder im Refresh liegt.

Achte auch auf Namensgebung. Ein Repository sollte nicht wie eine technische Tabelle heißen, wenn es fachlich etwas anderes anbietet. UserRepository ist oft besser als UserApiRepository, wenn es nicht nur die API kapselt, sondern auch lokale Daten und Cache-Regeln. Umgekehrt sollte eine RemoteDataSource nicht plötzlich UI-State bauen. Gute Namen machen Code-Reviews leichter, weil sie zeigen, ob eine Klasse gerade ihre Rolle verlässt.

Fazit

Die Data Layer ist der Teil deiner Android-App, der aus einzelnen Datenquellen einen verlässlichen Datenfluss macht. Sources beschaffen oder speichern Daten, Repositories bündeln die fachliche Sicht darauf, und Models halten die Grenzen zwischen API, Speicher, Domain und UI sauber. Prüfe dein Verständnis aktiv: Zeichne für einen Screen den Weg der Daten von Remote oder Local bis zum Compose-State, setze Breakpoints an jeder Schichtgrenze, schreibe einen kleinen Repository-Test mit Fake-Quellen und achte im Code-Review darauf, ob UI-Code technische Datenquellen kennt. Genau dort erkennst du, ob deine Architektur nur auf dem Papier ordentlich wirkt oder im App-Alltag wirklich trägt.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

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