Android Coden
Android 7 min lesen

Repository-Implementierung in Android

Du lernst, wie ein Repository lokale und entfernte Datenquellen bündelt. Der Fokus liegt auf klaren APIs, Mapping und Offline-Fähigkeit.

Eine Repository-Implementierung ist der Teil deiner Android-App, der Datenquellen zusammenführt und nach außen eine passende API für ein Feature anbietet. Statt dass ein ViewModel direkt mit Retrofit, Room, DataStore und Mapping-Code arbeitet, spricht es mit einem Repository. Dadurch bleibt deine App wartbar, testbar und besser auf reale Bedingungen vorbereitet: langsames Netz, leere Caches, geänderte Serverantworten und lokale Änderungen, die später synchronisiert werden müssen.

Was ist das?

Ein Repository ist eine Klasse oder Schnittstelle in der Data Layer deiner App. Es kapselt, woher Daten kommen und wie sie gespeichert, aktualisiert oder kombiniert werden. Die Implementierung entscheidet zum Beispiel, ob eine Liste zuerst aus einer lokalen Room-Datenbank gelesen wird, ob parallel ein Remote-Request per Retrofit startet und wie die Antwort anschließend in lokale Entities geschrieben wird.

Das mentale Modell ist einfach: Das Repository ist nicht „die Datenbank“ und auch nicht „der API-Client“. Es ist die fachliche Zugangsstelle zu Daten für einen bestimmten Bereich. Ein ArticleRepository sollte also nicht nur technische Methoden wie getFromApi() oder insertEntity() anbieten, sondern Methoden, die zum Feature passen: observeArticles(), refreshArticles() oder markAsRead(articleId).

In modernem Android passt dieses Muster gut zu Kotlin, Jetpack Compose, ViewModels, Coroutines und Flow. Compose beobachtet UI-State, das ViewModel formt daraus Zustände für den Bildschirm, und das Repository liefert Datenströme oder suspendierende Operationen. Die UI muss nicht wissen, ob die Daten lokal, remote oder aus einer Kombination stammen. Diese Trennung ist kein Selbstzweck. Sie verhindert, dass technische Details in jede Bildschirmklasse wandern und dort schwer testbar werden.

Wichtig ist auch die Grenze des Repositorys. Es soll Datenquellen kombinieren, aber nicht die gesamte App-Logik übernehmen. Validierungen, die eng zur Eingabe auf einem Bildschirm gehören, bleiben oft im ViewModel oder in einer separaten Use-Case-Klasse. Persistenz, Synchronisation, Mapping und Datenzugriff gehören dagegen meist in oder unter das Repository. Je größer die App wird, desto wertvoller wird diese klare Zuständigkeit.

Wie funktioniert es?

Eine typische Repository-Implementierung arbeitet mit drei Bausteinen: lokalen Datenquellen, entfernten Datenquellen und Mapping. Die lokale Quelle ist häufig Room oder DataStore. Die entfernte Quelle ist häufig ein Retrofit-Service oder ein anderer Netzwerk-Client. Mapping verbindet die Modelle zwischen diesen Schichten.

Warum brauchst du Mapping? Weil ein API-Modell selten dauerhaft das beste Modell für deine Datenbank oder UI ist. Ein Server liefert vielleicht snake_case, optionale Felder, verschachtelte Objekte oder Werte, die nur für die Synchronisation relevant sind. Deine Room-Entity braucht dagegen stabile Primärschlüssel, Indizes und Spalten. Dein UI-Modell braucht lesbare Texte, Flags und oft nur einen Ausschnitt der Daten. Wenn du überall dasselbe Modell verwendest, sparst du am Anfang Code, zahlst aber später mit Kopplung.

In einer sauberen Implementierung sieht das Repository nach außen fachlich aus. Intern kann es mehrere Quellen nutzen. Ein häufiges Offline-first-Muster lautet: Die App beobachtet lokale Daten, und Remote-Daten aktualisieren diese lokale Quelle. Dadurch bleibt die UI auch ohne Netz benutzbar. Das ViewModel sammelt zum Beispiel einen Flow<List<Article>> aus dem Repository. Dieser Flow kommt aus Room. Wenn refreshArticles() erfolgreich ist, schreibt das Repository neue Daten in die Datenbank. Room sendet automatisch neue Werte, und die UI aktualisiert sich.

Coroutines helfen dir dabei, blockierende Arbeit sauber auszulagern. Netzwerkzugriffe laufen suspendierend. Datenbankoperationen laufen über Room ebenfalls coroutine-freundlich. Für kontinuierliche Daten nutzt du Flow. Dabei solltest du bewusst entscheiden, was ein Stream ist und was ein einzelner Befehl ist. observeArticles() ist ein Stream, weil sich lokale Daten ändern können. refreshArticles() ist eher ein Befehl, weil er eine Aktualisierung auslöst und Erfolg oder Fehler melden kann.

Eine praktische Regel: Das Repository sollte nicht einfach alle Methoden seiner Datenquellen nach außen durchreichen. Wenn dein Repository nur api.getArticles() und dao.getArticles() spiegelt, hast du keine Abstraktion gebaut, sondern nur eine zusätzliche Schicht ohne Fachwert. Richte die API an den Bedürfnissen des Features aus. Ein Feed-Bildschirm braucht vielleicht „Artikel beobachten“ und „Feed aktualisieren“. Er braucht nicht zu wissen, wie viele Tabellen oder Endpunkte dahinter liegen.

Fehlerbehandlung gehört ebenfalls zur Implementierung. Ein Remote-Request kann fehlschlagen, während lokale Daten vorhanden sind. In einer Offline-first-App ist das nicht automatisch ein leerer Bildschirm. Du kannst lokale Daten weiter anzeigen und den Refresh-Fehler separat melden. Dafür brauchst du eine klare Rückgabeform, zum Beispiel Result, eine eigene Fehlerklasse oder einen Synchronisationsstatus in der Datenbank. Entscheidend ist, dass die UI nicht raten muss, ob ein Fehler bedeutet: „keine Daten“, „alte Daten“, „kein Netz“ oder „Serverproblem“.

In der Praxis

Stell dir ein Feature vor, das Artikel anzeigen soll. Die App soll vorhandene Artikel sofort aus der lokalen Datenbank zeigen und bei Bedarf vom Server aktualisieren. Das Repository verbindet also local, remote und mapping.

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

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

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

    override suspend fun refreshArticles(): Result<Unit> {
        return runCatching {
            val remoteArticles = api.getArticles()
            val entities = remoteArticles.map { it.toEntity() }
            dao.replaceAll(entities)
        }
    }
}

data class Article(
    val id: String,
    val title: String,
    val isRead: Boolean
)

data class ArticleDto(
    val id: String,
    val headline: String,
    val read: Boolean?
)

@Entity(tableName = "articles")
data class ArticleEntity(
    @PrimaryKey val id: String,
    val title: String,
    val isRead: Boolean
)

fun ArticleDto.toEntity(): ArticleEntity =
    ArticleEntity(
        id = id,
        title = headline,
        isRead = read ?: false
    )

fun ArticleEntity.toDomain(): Article =
    Article(
        id = id,
        title = title,
        isRead = isRead
    )

Dieses Beispiel ist bewusst klein, zeigt aber die zentrale Idee. Das ViewModel würde observeArticles() sammeln und daraus UI-State bauen. Ein Pull-to-refresh oder ein Start-Event kann refreshArticles() auslösen. Die UI bekommt keine DTOs und keine Entities. Sie arbeitet mit dem Domain-Modell Article, das für den App-Code verständlich ist.

Eine typische Stolperfalle ist das direkte Verwenden von DTOs in Compose. Das wirkt bequem, weil du weniger Klassen schreiben musst. Später ändert der Server aber headline zu title, oder read wird entfernt. Dann bricht plötzlich UI-Code, obwohl sich nur ein Netzwerkvertrag geändert hat. Mit Mapping bleibt die Änderung lokal begrenzt: Du passt ArticleDto und toEntity() an. Der Rest der App bleibt stabil, solange dein Domain-Modell fachlich gleich bleibt.

Eine zweite Stolperfalle ist eine unklare Quelle der Wahrheit. Wenn du manchmal direkt Remote-Daten anzeigst und manchmal lokale Daten, entstehen leicht widersprüchliche Zustände. Der Nutzer sieht nach einem Refresh andere Daten, nach einer Rotation wieder alte Daten, oder ein Offline-Fall wird nicht reproduzierbar. Für viele Listen- und Detailansichten ist eine gute Entscheidungsregel: Die UI beobachtet lokale Daten; Remote aktualisiert lokal. Ausnahmen gibt es, etwa reine Suchvorschläge ohne Speicherung, aber sie sollten bewusst gewählt werden.

Auch Transaktionen sind wichtig. Wenn du mehrere Tabellen aktualisierst, sollte die lokale Datenbank nicht kurz in einem halbfertigen Zustand stehen. Room bietet dafür Transaktionen. Für einen echten Feed würdest du zum Beispiel Artikel, Autoren und Cross-Reference-Tabellen gemeinsam aktualisieren. Das Repository oder die darunterliegende lokale Datenquelle sollte diese Operation als zusammenhängenden Schreibvorgang behandeln.

Beim Testen kannst du die Repository-Implementierung gut isolieren. Du ersetzt ArticleDao und ArticleApi durch Fakes und prüfst konkrete Regeln: Werden DTOs korrekt gemappt? Bleiben lokale Daten sichtbar, wenn der Refresh fehlschlägt? Wird replaceAll() mit den erwarteten Entities aufgerufen? Für Flow-basierte APIs prüfst du, ob nach einer lokalen Änderung der neue Domain-Wert ausgesendet wird. Solche Tests sind wertvoll, weil sie nicht nur einzelne Funktionen prüfen, sondern das Datenverhalten deines Features.

Im Code-Review solltest du bei Repositorys besonders auf Namen und Grenzen achten. Eine Methode wie getData() sagt zu wenig. Eine Methode wie observeBookmarkedArticles() erklärt dagegen, welche fachliche Sicht geliefert wird. Prüfe außerdem, ob Mapping-Funktionen an einer nachvollziehbaren Stelle liegen, ob Fehler nicht verschluckt werden und ob das ViewModel keine DAO- oder Retrofit-Typen importiert. Wenn ein ViewModel ArticleDto kennt, ist das oft ein Hinweis, dass die Data Layer durch die Architektur nach oben leckt.

Für Junior-Entwickler ist noch ein Punkt wichtig: Ein Repository muss nicht immer komplex sein. Wenn ein Feature nur eine einzige lokale Einstellung aus DataStore liest, kann die Implementierung sehr klein sein. Der Wert des Musters liegt nicht in viel Code, sondern in einer stabilen Grenze. Sobald lokale und entfernte Daten, Caching, Mapping oder Offline-Verhalten dazukommen, zahlt sich diese Grenze aus.

Fazit

Eine gute Repository-Implementierung bündelt lokale und entfernte Datenquellen hinter einer API, die zum Feature passt. Sie schützt ViewModels und Compose-UI vor technischen Details, hält Mapping an einer kontrollierten Stelle und macht Offline-first-Verhalten nachvollziehbar. Prüfe dein Verständnis aktiv: Baue ein kleines Repository mit Room-Fake und API-Fake, schreibe Tests für erfolgreiches und fehlgeschlagenes Refreshing, und kontrolliere im Debugger, ob die UI wirklich nur Domain-Modelle sieht.

Quellen (4)
Redaktion

Geschrieben von

Redaktion

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