Retrofit + Coroutines

The previous lesson's TrailRepository called api.getTrails() without ever explaining what api actually is. It's an instance of Retrofit — the standard, type-safe HTTP client library for Android — configured to talk to whatever backend serves TrailLog's trail data. This lesson builds that piece.

Defining an API as a Kotlin Interface

Retrofit's core idea is that you describe a web API as a plain Kotlin interface, and Retrofit generates the actual networking code behind it at runtime. Here's the simplest possible version for TrailLog:

interface TrailApiService {

    @GET("trails")
    suspend fun getTrails(): List<Trail>
}

The suspend modifier is the entire integration point with coroutines — nothing else is needed. Retrofit detects that a method is a suspend function and automatically runs the network call off the main thread, resuming the calling coroutine with the result once it completes. Compare that to older Retrofit-with-Java code, which needed a Call<List<Trail>> return type plus a manual enqueue() call with success/failure callbacks — real boilerplate that a one-word suspend modifier now eliminates entirely.

Building the Retrofit Instance

A Retrofit instance needs a base URL and a converter factory — the piece responsible for turning raw JSON bytes into Kotlin objects (and back). Here it's configured with a JSON converter; exactly which converter library to reach for, and why, is covered in full in JSON & kotlinx.serialization right after this lesson:

val retrofit = Retrofit.Builder()
    .baseUrl("https://api.traillogapp.example.com/")
    .addConverterFactory(Json.asConverterFactory("application/json".toMediaType()))
    .build()

val trailApi: TrailApiService = retrofit.create(TrailApiService::class.java)

retrofit.create() is where the generated implementation actually comes from — you never write a class that implements TrailApiService yourself; Retrofit builds one for you based purely on the interface's method signatures and annotations.

Calling It From Inside the Repository

With trailApi built, plugging it into last lesson's TrailRepository is just passing it in as a constructor argument — the suspend call inside getTrails() reads like ordinary sequential code, with no callbacks in sight:

class TrailRepository(
    private val trailDao: TrailDao,
    private val api: TrailApiService
) {
    suspend fun getTrails(): List<Trail> {
        return try {
            val freshTrails = api.getTrails() // suspends here, resumes with the result
            trailDao.insertAll(freshTrails)
            freshTrails
        } catch (e: Exception) {
            trailDao.getAllTrails()
        }
    }
}

The Full Annotation Set: @GET, @POST, @Path, @Query, @Body

Retrofit's request-shape annotations cover the vast majority of real API calls:

AnnotationPurpose
@GETreads data — no request body, matches an HTTP GET
@POSTcreates or submits data, paired with @Body for the payload
@Pathsubstitutes a value into a placeholder segment of the URL path, like an id
@Queryappends a value as a URL query parameter, like ?limit=20
@Bodymarks a parameter as the full request payload, serialized to JSON

Fetching one specific trail by id uses @Path to fill in the URL:

@GET("trails/{id}")
suspend fun getTrailById(@Path("id") trailId: Long): Trail

Filtering the list with a query parameter uses @Query:

@GET("trails")
suspend fun getTrails(@Query("min_distance_km") minDistanceKm: Double? = null): List<Trail>

And submitting a brand-new trail sends the whole Trail object as JSON with @Body:

@POST("trails")
suspend fun createTrail(@Body trail: Trail): Trail

Kotlin's Default Parameters Fit Retrofit Naturally

Notice minDistanceKm: Double? = null above — a normal Kotlin default parameter. Since Retrofit generates its implementation from the interface signature at the language level, Kotlin features like default parameter values and named arguments work exactly as you'd expect when calling these functions: trailApi.getTrails(minDistanceKm = 5.0) reads clearly, and omitting the argument entirely still compiles and simply sends no query parameter at all.

Note: Every interface and Retrofit setup snippet above is correct, real Kotlin code — but this website's own project doesn't run a live Android Studio build, so no request actually fires from this page.
Up next: Retrofit doesn't send requests itself — it hands them off to a lower-level HTTP client. OkHttp & Interceptors covers that client, and how to inspect and modify every request that passes through it.