Android Coden
Android 4 min lesen

Repository APIs

Repositories kapseln Datenquellen hinter einer klaren API. Du lernst, Methoden app-nah statt datenbank-nah zu gestalten.

Das Repository-Muster gehört zu den Architekturentscheidungen, die den größten Einfluss auf die langfristige Wartbarkeit einer Android-App haben. Es trennt die Datenbeschaffung von der Anzeigelogik so sauber, dass ViewModel und UI nie direkt mit Datenbank oder Netzwerk sprechen müssen. Wer Repository-APIs konsequent app-nah entwirft, gewinnt klare Schnittstellen, leicht testbare Schichten und eine Architektur, die einen Quellwechsel übersteht, ohne den gesamten Code umzuschreiben.

Was ist das?

Ein Repository ist eine Klasse in der Datenschicht deiner App, die alle Datenquellen – lokale Datenbank, Remote-API, In-Memory-Cache – hinter einer einzigen, domänenorientierten Schnittstelle versteckt. Der Rest der App fragt nie: „Kommen diese Daten aus Room oder von Retrofit?” Er fragt nur: „Gib mir die aktuellen Artikel.” Das Repository entscheidet intern, welche Quelle herangezogen wird, wie Konflikte zwischen lokalen und entfernten Daten aufgelöst werden und wann ein Cache noch frisch genug ist.

In Googles empfohlener Android-Architektur bildet das Repository die oberste Klasse der Datenschicht. Darüber liegt die UI-Schicht mit ViewModel und State, darunter befinden sich konkrete Data Sources wie Room-DAOs, Retrofit-Services oder DataStore. Das Repository ist der Grenzwächter: Es nimmt Anfragen in der Sprache der App entgegen und übersetzt sie in Aufrufe gegen konkrete Backends. Dieser Puffer erlaubt es dir, eine Datenbank-Implementierung auszutauschen oder eine neue Remote-Quelle hinzuzufügen, ohne eine einzige Zeile im ViewModel anzufassen.

Wie funktioniert es?

Repository-Methoden basieren auf zwei Kotlin-Primitiven: suspend-Funktionen und Flow.

Suspend-Funktionen eignen sich für einmalige Operationen, die ein Ergebnis liefern und dann enden – etwa einen Nutzer anlegen, ein Token erneuern oder einen Eintrag löschen. Die aufrufende Coroutine wartet, bis das Repository antwortet, ohne den Hauptthread zu blockieren. Das Ergebnis wird einmalig zurückgegeben.

Flow eignet sich für reaktive Datenströme, die sich im Laufe der Zeit ändern können – beispielsweise eine Artikelliste, die Room automatisch aktualisiert, sobald sich die Datenbank ändert. Das ViewModel sammelt diesen Flow und stellt ihn als StateFlow oder via collectAsState bereit.

interface ArticleRepository {
    fun getArticles(): Flow<List<Article>>
    suspend fun refreshArticles()
    suspend fun toggleBookmark(articleId: String)
}

Beachte: Die Schnittstelle spricht in App-Domänen (Article, toggleBookmark), nicht in Datenbankbegriffen (ArticleEntity, UPDATE bookmarked = 1). Die Implementierung übernimmt die Übersetzung:

class ArticleRepositoryImpl(
    private val dao: ArticleDao,
    private val api: ArticleApi
) : ArticleRepository {

    override fun getArticles(): Flow<List<Article>> =
        dao.observeAll().map { entities -> entities.map { it.toArticle() } }

    override suspend fun refreshArticles() {
        val remote = api.fetchArticles()
        dao.upsertAll(remote.map { it.toEntity() })
    }

    override suspend fun toggleBookmark(articleId: String) {
        dao.toggleBookmark(articleId)
    }
}

Das ViewModel injiziert ArticleRepository über Hilt, sammelt den Flow und weiß zu keinem Zeitpunkt, ob Daten lokal oder remote stammen.

In der Praxis

Methoden app-nah entwerfen

Die häufigste Stolperfalle beim Entwurf von Repository-APIs ist das direkte Spiegeln der Datenbankstruktur. Eine Methode wie getArticlesByCategoryId(categoryId: Int) sieht harmlos aus, zwingt das ViewModel aber, intern mit numerischen IDs zu arbeiten – obwohl die App ausschließlich mit Category-Enums denkt. Die Übersetzungslogik wandert ins ViewModel, das Repository verliert seinen Trennwert.

Regel: Entwirf jede Repository-Methode aus der Perspektive des ViewModel. Was fragt der Screen wirklich? Genau das liefert das Repository – in App-Typen, nicht in Datenbanktypen. Wenn sich die Datenbankstruktur ändert, bleibt die Repository-API stabil, und nur die interne Übersetzungslogik muss angepasst werden.

Fehlerbehandlung im Flow

Ein weiterer Stolperstein: Unbehandelte Ausnahmen in einem Flow beenden den Datenstrom sofort. Wenn api.fetchArticles() einen Netzwerkfehler wirft und du ihn nicht abfängst, friert die UI ein, weil der Flow kein weiteres Element mehr liefert. Gib Fehler stattdessen als Result<T> oder über einen Sealed-Typ weiter:

override fun getArticles(): Flow<Result<List<Article>>> =
    dao.observeAll()
        .map { entities -> Result.success(entities.map { it.toArticle() }) }
        .catch { e -> emit(Result.failure(e)) }

So kann das ViewModel auf Fehler reagieren, ohne den Flow neu starten zu müssen.

Testbarkeit durch Interfaces

Weil ArticleRepository ein Interface ist, kannst du in Unit-Tests eine FakeArticleRepository-Implementierung injizieren, die intern einen MutableStateFlow hält. Du steuerst den Zustand direkt aus dem Test heraus, ohne Datenbank oder Netzwerk zu benötigen. Das macht Tests schnell, deterministisch und parallelisierbar – ein direkter Mehrwert des sauberen Vertrags.

Fazit

Repository-APIs sind der Klebstoff zwischen App-Logik und Außenwelt. Wer Methoden konsequent in der Sprache der App definiert, Suspend-Funktionen für Einmaloperationen und Flows für reaktive Ströme einsetzt und Fehler als Werte weitergibt, baut eine Datenschicht, die sich sowohl leicht testen als auch leicht erweitern lässt. Nimm dir in deinem nächsten Code-Review eine Minute und prüfe jede Repository-Methode: Kann ich ihren Namen verstehen, ohne das Datenbankschema oder die API-Dokumentation zu kennen? Wenn nicht, ist das ein sicheres Zeichen, dass der Vertrag noch zu nah an der Implementierung klebt – und das ist der beste Zeitpunkt, ihn zu schärfen.

Quellen (6)
Redaktion

Geschrieben von

Redaktion

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