Android Coden
Android 9 min lesen

Retrofit-Konzepte für typisierte REST-Aufrufe

Retrofit beschreibt typisierte REST-Aufrufe in Android. Du lernst Interfaces, Annotationen und den HTTP-Client einzuordnen.

Retrofit ist in vielen Android-Projekten der Standardweg, um REST-APIs aus Kotlin heraus typisiert anzusprechen. Statt URLs, Header, JSON und Fehlerfälle überall in deiner App zu verteilen, beschreibst du einen Webservice als Interface und lässt Retrofit daraus konkrete HTTP-Aufrufe erzeugen. Das passt gut zur modernen Android-Architektur: Netzwerkzugriffe liegen in der Datenebene, ViewModels arbeiten gegen Repositories, und Compose bekommt am Ende nur den aufbereiteten UI-State.

Was ist das?

Retrofit ist eine Bibliothek für HTTP-Kommunikation, die dir hilft, REST-Endpunkte als Kotlin- oder Java-Interfaces zu modellieren. Der zentrale Gedanke ist: Du beschreibst, was ein Request tun soll, nicht wie jedes Byte über die Leitung geht. Eine Funktion im Interface steht für einen API-Endpunkt. Annotationen wie @GET, @POST, @Path, @Query oder @Body beschreiben Methode, URL-Teile, Parameter und Nutzdaten. Retrofit verbindet diese Beschreibung mit einem HTTP-Client, meist OkHttp, und mit einem Converter, zum Beispiel für JSON.

Für Android-Lernende ist dieses Modell wichtig, weil reale Apps selten nur lokale Daten anzeigen. Wetter, Profile, Produkte, Chats, Zahlungsdaten oder Konfigurationen kommen oft aus einem Backend. Ohne klare Struktur entsteht schnell Code, der schwer testbar ist: ein bisschen URL-Bau im ViewModel, ein bisschen JSON-Parsing im Composable, ein paar try-catch-Blöcke in verschiedenen Dateien. Retrofit löst nicht alle Architekturfragen, aber es gibt dir eine saubere technische Grenze für REST-Aufrufe.

Im Roadmap-Kontext gehört Retrofit in die Datenebene. Die offizielle Android-Architektur beschreibt diese Ebene als zuständig für Datenquellen, Repositories und die Bereitstellung von App-Daten für andere Schichten. Retrofit ist dabei typischerweise eine Remote-Datenquelle. Es sollte nicht direkt aus einer Compose-Funktion aufgerufen werden, weil UI-Code dadurch Netzwerkdetails kennen müsste. Stattdessen ruft ein Repository den Retrofit-Service auf, entscheidet über Mapping und Fehlerbehandlung und liefert dem ViewModel ein Ergebnis, das zur UI passt.

Das mentale Modell für den Einstieg ist sehr einfach zu merken, ohne die Technik zu unterschätzen: Ein Retrofit-Service-Interface ist ein Vertrag zwischen deiner App und dem Backend. Die Annotationen sind die Wegbeschreibung für den HTTP-Request. Der HTTP-Client führt den Request tatsächlich aus. Converter übersetzen zwischen JSON und Kotlin-Datenklassen. Deine Repository-Schicht entscheidet, was diese Antwort für die App bedeutet.

Wie funktioniert es?

Retrofit arbeitet zur Laufzeit mit einer erzeugten Implementierung deines Service-Interfaces. Du schreibst also nur das Interface, nicht die Klasse, die den Request manuell ausführt. Beim Erstellen einer Retrofit-Instanz gibst du mindestens eine baseUrl und meist einen Converter an. Danach rufst du retrofit.create(MyApi::class.java) auf. Das zurückgegebene Objekt implementiert dein Interface. Wenn du eine Funktion darauf aufrufst, liest Retrofit die Annotationen, baut daraus einen HTTP-Request, übergibt ihn an den HTTP-Client und wandelt die Antwort in den deklarierten Rückgabetyp.

Die wichtigsten Bausteine sind Service-Interfaces, Annotationen und HTTP-Client. Das Service-Interface sollte fachlich zusammenhängende Endpunkte bündeln. Ein UserApi enthält etwa Benutzer-Endpunkte, ein ArticleApi Artikel-Endpunkte. Zu große Interfaces werden unübersichtlich, zu viele winzige Interfaces erzeugen unnötige Verdrahtung. Eine gute Regel ist: Teile nach Backend-Ressourcen oder klaren fachlichen Bereichen, nicht nach einzelnen Screens.

Annotationen sind der Teil, den Anfänger oft zuerst sehen. @GET("users/{id}") beschreibt einen GET-Request mit einem Pfadplatzhalter. @Path("id") füllt diesen Platzhalter. @Query("page") hängt einen Query-Parameter an. @Body serialisiert eine Datenklasse in den Request-Body, meistens als JSON. @Header oder @Headers ergänzen HTTP-Header. Diese Annotationen sind kein Dekor, sondern der eigentliche Vertrag für den Request. Ein falsch geschriebener Pfad oder ein fehlender Query-Parameter führt nicht zu einem Compilerfehler gegen dein Backend, sondern häufig zu Laufzeitfehlern oder unerwarteten Serverantworten.

Der HTTP-Client ist meist OkHttp. Dort konfigurierst du Timeouts, Logging, Authentifizierung, Interceptors, Caching und TLS-Verhalten. Retrofit kümmert sich um die Service-Abstraktion, OkHttp um die konkrete Netzwerkebene. Diese Trennung ist nützlich, weil du globale Regeln an einer Stelle definieren kannst. Ein Auth-Interceptor kann zum Beispiel bei jedem Request ein Token hinzufügen. Ein Logging-Interceptor kann in Debug-Builds Requests sichtbar machen, ohne jeden Service-Aufruf anzufassen. In Release-Builds musst du dabei vorsichtig sein, weil Logs sensible Daten enthalten können.

Mit Kotlin werden Retrofit-Funktionen häufig als suspend deklariert. Dadurch passen sie in Coroutines und können aus ViewModels oder Repositories sauber aufgerufen werden. Wichtig ist aber: Eine suspend-Funktion macht Netzwerkcode nicht automatisch architektonisch sauber. Sie sagt nur, dass der Aufruf pausieren kann, ohne den Thread blockierend zu belegen. Du brauchst weiterhin eine klare Fehlerstrategie. Du musst entscheiden, ob dein Service Dto direkt zurückgibt, Response<Dto> verwendet oder ob du im Repository eigene Ergebnis-Typen wie Result, Either oder eine sealed class nutzt.

In einer Offline-First-Architektur ist Retrofit nur eine Datenquelle unter mehreren. Die App kann zuerst lokale Daten aus einer Datenbank anzeigen und das Netzwerk zur Synchronisation nutzen. Retrofit lädt dann frische Daten, während Room oder ein anderer lokaler Speicher die stabile Quelle für die UI bleibt. Das verhindert, dass deine UI bei jeder schlechten Verbindung leer bleibt. Der Retrofit-Aufruf wird dadurch nicht unwichtig, aber seine Rolle ändert sich: Er ist nicht mehr die einzige Wahrheit, sondern ein Weg, um den lokalen Zustand zu aktualisieren.

Ein weiterer wichtiger Punkt ist das Mapping. Servermodelle, oft DTOs genannt, sollten nicht automatisch deine Domain- oder UI-Modelle sein. Ein Backend kann Feldnamen, optionale Werte oder verschachtelte Strukturen liefern, die nicht gut zu deiner App passen. Wenn du diese DTOs ungeprüft bis in Compose weiterreichst, verteilt sich Backend-Wissen durch die ganze App. In der Datenebene kannst du stattdessen aus UserDto ein User-Modell machen und fehlende oder ungültige Felder bewusst behandeln.

In der Praxis

Stell dir vor, du baust eine App, die Artikel von einem Backend lädt. Der Screen zeigt eine Liste, aber Retrofit sollte nicht im Composable auftauchen. Du legst zuerst ein Service-Interface an, dann eine Repository-Klasse, die dieses Interface nutzt. Das ViewModel ruft später das Repository auf und wandelt das Ergebnis in UI-State. So bleibt die UI unabhängig von HTTP-Details.

Ein typisches Retrofit-Interface kann so aussehen:

interface ArticleApi {
    @GET("articles")
    suspend fun getArticles(
        @Query("page") page: Int,
        @Query("limit") limit: Int
    ): List<ArticleDto>

    @GET("articles/{id}")
    suspend fun getArticleById(
        @Path("id") id: String
    ): ArticleDto

    @POST("articles")
    suspend fun createArticle(
        @Body request: CreateArticleRequest
    ): ArticleDto
}

data class ArticleDto(
    val id: String,
    val title: String,
    val body: String?,
    val updatedAt: String
)

data class CreateArticleRequest(
    val title: String,
    val body: String
)

Die Retrofit-Konfiguration ist meist Teil deiner Dependency-Injection-Struktur. In kleinen Projekten kann sie in einem Modul oder einer Factory liegen. In größeren Apps wird sie zum Beispiel mit Hilt bereitgestellt. Entscheidend ist, dass du nicht überall neue Retrofit-Instanzen erzeugst. Eine zentrale Konfiguration reduziert Fehler und macht Authentifizierung, Logging und Tests einfacher.

fun provideArticleApi(): ArticleApi {
    val client = OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    val retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(client)
        .addConverterFactory(MoshiConverterFactory.create())
        .build()

    return retrofit.create(ArticleApi::class.java)
}

Im Repository entscheidest du, wie technische Antworten in App-Verhalten übersetzt werden. Wenn du direkt List<ArticleDto> zurückgibst, wirft Retrofit bei Netzwerkproblemen Exceptions. Bei HTTP-Fehlern hängt das genaue Verhalten vom Rückgabetyp ab. Mit Response<List<ArticleDto>> kannst du Statuscodes explizit prüfen. Beide Varianten sind gültig, aber du solltest bewusst wählen. Für Lernprojekte ist Response<T> oft hilfreich, weil du siehst, dass ein erfolgreicher Transport und ein fachlich nutzbarer Inhalt zwei verschiedene Dinge sind.

class ArticleRepository(
    private val api: ArticleApi
) {
    suspend fun loadArticles(page: Int): ArticleLoadResult {
        return try {
            val response = api.getArticlesResponse(page = page, limit = 20)

            if (response.isSuccessful) {
                val body = response.body().orEmpty()
                ArticleLoadResult.Success(
                    articles = body.map { dto ->
                        Article(
                            id = dto.id,
                            title = dto.title,
                            body = dto.body.orEmpty()
                        )
                    }
                )
            } else {
                ArticleLoadResult.ServerError(response.code())
            }
        } catch (exception: IOException) {
            ArticleLoadResult.NetworkError
        }
    }
}

interface ArticleApi {
    @GET("articles")
    suspend fun getArticlesResponse(
        @Query("page") page: Int,
        @Query("limit") limit: Int
    ): Response<List<ArticleDto>>
}

sealed interface ArticleLoadResult {
    data class Success(val articles: List<Article>) : ArticleLoadResult
    data class ServerError(val statusCode: Int) : ArticleLoadResult
    data object NetworkError : ArticleLoadResult
}

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

Diese Struktur wirkt für Anfänger zunächst etwas länger als ein direkter API-Aufruf im ViewModel. Der Nutzen zeigt sich aber schnell. Du kannst das Repository testen, indem du das ArticleApi-Interface fälschst oder mit einem Testserver wie MockWebServer arbeitest. Du kannst prüfen, ob HTTP 500 korrekt als Serverfehler behandelt wird. Du kannst testen, ob ein fehlender body-Wert nicht die UI zum Absturz bringt. Und du kannst in Code-Reviews klar sehen, wo Netzwerkverhalten endet und App-Logik beginnt.

Eine typische Stolperfalle ist die baseUrl. Retrofit verlangt, dass sie mit einem Slash endet, zum Beispiel https://api.example.com/. Gleichzeitig sollten Pfade in Annotationen relativ dazu passen. Wenn du führende Slashes, doppelte Pfadteile oder unterschiedliche Umgebungen unkontrolliert mischst, entstehen schwer erkennbare URL-Fehler. Prüfe deshalb bei Problemen zuerst die tatsächlich gesendete URL, etwa mit einem Logging-Interceptor in Debug-Builds oder mit einem Testserver.

Eine zweite Stolperfalle ist das Verwechseln von DTO und UI-Modell. Nur weil Moshi oder Gson eine JSON-Antwort direkt in eine Datenklasse schreiben kann, heißt das nicht, dass diese Datenklasse überall genutzt werden sollte. Ein DTO darf nullable Felder, technische Namen oder servernahe Strukturen enthalten. Ein UI-Modell sollte dagegen ausdrücken, was der Screen wirklich braucht. Wenn du diese Trennung einhältst, bleiben spätere API-Änderungen kleiner und leichter zu testen.

Eine dritte Stolperfalle betrifft Fehlerbehandlung. Viele Beispiele zeigen nur den erfolgreichen Fall. In echten Apps musst du mindestens Netzwerkfehler, HTTP-Fehler, leere Antworten und fehlerhafte Daten unterscheiden. Das heißt nicht, dass jeder Fehler einen eigenen Dialog braucht. Aber dein Code sollte nicht so tun, als sei jede Antwort erfolgreich. Besonders bei Offline-First-Ansätzen ist diese Unterscheidung wichtig: Ein Netzwerkfehler kann bedeuten, dass du lokale Daten weiter anzeigst und später synchronisierst. Ein Authentifizierungsfehler kann dagegen eine neue Anmeldung erfordern.

Für die tägliche Arbeit kannst du dir eine klare Entscheidungsregel merken: Retrofit beschreibt nur den Remote-Zugriff; die Bedeutung für deine App gehört ins Repository. Wenn du in einem Composable @GET, Response, HTTP-Statuscodes oder DTO-Namen siehst, ist die Schichtengrenze wahrscheinlich verletzt. Wenn dein ViewModel dagegen mit einem Repository spricht und einen UI-State erzeugt, ist die Richtung sauberer. Das ist keine starre Vorschrift für jedes Mini-Projekt, aber eine gute Orientierung für Code, der wachsen soll.

Auch die Validierung deines Verständnisses sollte praktisch sein. Nimm einen kleinen Endpunkt, definiere ein Retrofit-Interface und schreibe einen Test mit einer simulierten JSON-Antwort. Ändere dann absichtlich den Statuscode auf 404, entferne ein Feld aus der Antwort und simuliere einen Verbindungsfehler. Beobachte im Debugger, welche Codepfade laufen. Wenn du danach erklären kannst, welche Rolle Interface, Annotation, HTTP-Client, Converter und Repository jeweils spielen, hast du das Konzept verstanden.

Fazit

Retrofit gibt dir einen klaren, typisierten Zugang zu REST-APIs, aber der eigentliche Qualitätsgewinn entsteht durch die richtige Einordnung in deine Android-Architektur. Service-Interfaces beschreiben Endpunkte, Annotationen definieren die HTTP-Details, der HTTP-Client führt Requests aus, und das Repository übersetzt technische Antworten in App-Verhalten. Prüfe dein nächstes Retrofit-Beispiel aktiv: Setze einen Breakpoint im Repository, teste Erfolg und Fehlerfälle, kontrolliere die erzeugte URL und achte im Code-Review darauf, dass keine Netzwerkdetails in Compose oder unnötig tief in den UI-State rutschen.

Quellen (2)
Redaktion

Geschrieben von

Redaktion

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