Networking with Retrofit
Retrofit is a type-safe HTTP client for Android and Java built on top of OkHttp. Rather than manually constructing HttpURLConnection requests, parsing raw JSON, and managing threads, you describe your API as a Kotlin interface annotated with HTTP verbs, path parameters, query parameters, and request bodies. Retrofit's runtime generates an implementation of that interface using dynamic proxies, translating each annotated function call into an actual network request and converting the response body into a Kotlin object through a pluggable converter, most commonly Moshi or kotlinx.serialization for JSON. Because each API function can be declared suspend, calling it from a coroutine suspends until the response arrives without blocking a thread, and errors surface as ordinary exceptions you can catch with try/catch or model explicitly with a sealed Result type.
Cricket analogy: Retrofit is like handing a translator a simple order-sheet of 'request scorecard for match 42' rather than personally wiring a satellite uplink, parsing raw broadcast signal, and formatting it yourself; declaring the function suspend is like the translator working during the tea break without stopping play.
Defining the API interface
Each function in a Retrofit interface corresponds to one endpoint. @GET, @POST, @PUT, @DELETE, and @PATCH annotations specify the HTTP method and the relative path, with {placeholders} filled in by parameters annotated @Path. Query parameters use @Query, and a JSON request body is supplied via a parameter annotated @Body, which Retrofit serializes using the configured converter. Response types are ordinary Kotlin data classes; Retrofit deserializes the JSON response into them field by field, matching property names unless a converter-specific annotation like @Json(name = ...) remaps a field.
Cricket analogy: Each Retrofit function is like a dedicated request form for a specific stadium action: @GET fetches today's scorecard with {matchId} filled from @Path, @Query narrows it to a specific innings, and @Body posts a new team-sheet submission that the server deserializes into a Player data class.
Building the Retrofit instance and error handling
A Retrofit instance is constructed once with Retrofit.Builder, given a baseUrl, a converter factory, and typically an OkHttpClient configured with interceptors for logging, authentication headers, or timeouts. That single instance then creates implementations of any number of API interfaces via retrofit.create(ApiInterface::class.java). Because network calls can fail for many reasons — no connectivity, a 4xx/5xx response, a malformed body — production code typically wraps each call in a try/catch that distinguishes IOException (connectivity-level failures) from HttpException (a well-formed response with a non-2xx status code), often mapping both into an app-specific sealed Result type so the UI layer never has to reason about Retrofit's own exception hierarchy directly.
Cricket analogy: Retrofit.Builder configured once with baseUrl and interceptors is like a broadcaster setting up one satellite uplink for the whole tournament, then reusing it for every match; distinguishing IOException (signal dropout) from HttpException (a malformed scorecard feed with a bad status) mirrors mapping both into one clean on-air graphic.
data class WeatherResponse(
val cityName: String,
val temperatureCelsius: Double,
val condition: String
)
interface WeatherApi {
@GET("v1/weather/{city}")
suspend fun getWeather(
@Path("city") city: String,
@Query("units") units: String = "metric"
): WeatherResponse
@POST("v1/favorites")
suspend fun addFavorite(@Body city: FavoriteRequest): Response<Unit>
}
object NetworkModule {
private val okHttpClient = OkHttpClient.Builder()
.addInterceptor(HttpLoggingInterceptor().apply { level = HttpLoggingInterceptor.Level.BASIC })
.build()
val weatherApi: WeatherApi = Retrofit.Builder()
.baseUrl("https://api.example.com/")
.client(okHttpClient)
.addConverterFactory(MoshiConverterFactory.create())
.build()
.create(WeatherApi::class.java)
}
suspend fun fetchWeather(city: String): Result<WeatherResponse> = try {
Result.success(NetworkModule.weatherApi.getWeather(city))
} catch (e: IOException) {
Result.failure(e) // no connectivity
} catch (e: HttpException) {
Result.failure(e) // non-2xx response
}Retrofit itself never performs the HTTP transaction — it delegates to OkHttp underneath. This is why interceptors, connection pooling, caching, and timeout configuration are all set on the OkHttpClient you hand to Retrofit.Builder.client(), not on Retrofit directly.
A frequent mistake is creating a new Retrofit (and OkHttpClient) instance per request or per screen. This throws away OkHttp's connection pool, forces a fresh TLS handshake on every call, and wastes battery and latency. Build one Retrofit instance and one OkHttpClient, and share both across the app, typically via dependency injection.
- Retrofit converts an annotated Kotlin interface into a working HTTP client via a generated dynamic proxy.
- @GET/@POST/@PUT/@DELETE with @Path, @Query, and @Body describe each endpoint declaratively.
- Suspend functions let API calls be awaited directly from coroutines without manual threading.
- A converter factory such as Moshi or kotlinx.serialization handles JSON (de)serialization.
- IOException signals connectivity failures; HttpException signals a non-2xx HTTP response.
- Retrofit and its OkHttpClient should be constructed once and shared, not recreated per call.
Practice what you learned
1. How does Retrofit turn an annotated Kotlin interface into a working HTTP client?
2. Which exception typically indicates the device could not reach the server at all (e.g. no connectivity)?
3. What is the role of a converter factory such as MoshiConverterFactory in a Retrofit setup?
4. Where should you configure interceptors, timeouts, and connection pooling in a Retrofit-based networking layer?
5. Why is it inefficient to create a new Retrofit instance for every network call?
Was this page helpful?
You May Also Like
Repository Pattern in Android
The repository pattern centralizes data access behind a single abstraction, hiding whether data comes from Room, Retrofit, or DataStore from ViewModels and the UI layer.
Coroutines Basics in Android
Kotlin coroutines provide a lightweight, structured way to write asynchronous, non-blocking code on Android using suspend functions, coroutine builders, dispatchers, and scopes.
Kotlin Flow Fundamentals
Flow is Kotlin's asynchronous stream type for emitting multiple sequential values over time, built on coroutines, and central to reactive state management in Android apps.
Unit Testing ViewModels
Testing ViewModels in isolation relies on fake repositories, a controlled coroutine dispatcher, and asserting on emitted StateFlow values without touching Android framework classes.