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:
| Annotation | Purpose |
|---|---|
@GET | reads data — no request body, matches an HTTP GET |
@POST | creates or submits data, paired with @Body for the payload |
@Path | substitutes a value into a placeholder segment of the URL path, like an id |
@Query | appends a value as a URL query parameter, like ?limit=20 |
@Body | marks 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.