Skip to content
intermediate Phase 8 · Networking

Retrofit

Define REST APIs with Retrofit: endpoints, request bodies, response models, and call adapters.

55m
4 problems
Topic Progress 0%

Setting Up Retrofit

What Is Retrofit?

Retrofit is a type-safe HTTP client for Android and Java, built on top of OkHttp. Instead of manually constructing URLs, serializing payloads, and parsing responses, you define your API as a Kotlin interface. Retrofit generates the implementation at runtime.

Why Retrofit Over Raw OkHttp?

OkHttp handles the connection layer - pooling, retries, interceptors. Retrofit sits above it and handles the application layer: converting your method calls into HTTP requests and your responses into Kotlin objects.

Basic Setup

// build.gradle.kts
implementation("com.squareup.retrofit2:retrofit:2.11.0")
implementation("com.squareup.retrofit2:converter-gson:2.11.0")
// Define your API
interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: Long): Response<User>

    @GET("users")
    suspend fun listUsers(
        @Query("page") page: Int,
        @Query("limit") limit: Int = 20
    ): Response<List<User>>
}

// Create the Retrofit instance
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val api = retrofit.create(UserApi::class.java)

The create() call uses a dynamic proxy to generate your interface implementation. Each annotated method maps directly to an HTTP request.

HTTP Annotations and Parameters

Method Annotations

Retrofit uses method-level annotations to declare the HTTP method and endpoint:

@GET("users/{id}")
suspend fun getUser(@Path("id") id: Long): Response<User>

@POST("users")
suspend fun createUser(@Body user: CreateUserRequest): Response<User>

@PUT("users/{id}")
suspend fun updateUser(
    @Path("id") id: Long,
    @Body user: UpdateUserRequest
): Response<User>

@DELETE("users/{id}")
suspend fun deleteUser(@Path("id") id: Long): Response<Unit>

@PATCH("users/{id}/settings")
suspend fun patchSettings(
    @Path("id") id: Long,
    @Body settings: Map<String, Any>
): Response<User>

Parameter Annotations

Annotation Purpose Example
@Path URL path segment users/{id} -> @Path("id") id: Long
@Query Query parameter ?page=1 -> @Query("page") page: Int
@QueryMap Multiple query params @QueryMap filters: Map<String, String>
@Header Single header @Header("Authorization") token: String
@Headers Static headers @Headers("X-API-Version: 2")
@Field Form-encoded field Used with @FormUrlEncoded
@Body Serialized request body Kotlin data class serialized by converter

Form-Encoded Requests

Some APIs require application/x-www-form-urlencoded instead of JSON:

@FormUrlEncoded
@POST("auth/login")
suspend fun login(
    @Field("username") username: String,
    @Field("password") password: String
): Response<AuthToken>

Multipart Uploads

@Multipart
@POST("uploads")
suspend fun uploadFile(
    @Part file: MultipartBody.Part,
    @Part("description") description: RequestBody
): Response<UploadResult>

Multipart is used for file uploads where the body contains multiple parts with different content types.

Production Patterns

Wrapping Responses with Sealed Classes

Raw Response<T> does not distinguish between network errors and API errors. A sealed class hierarchy handles this cleanly:

sealed class ApiResult<out T> {
    data class Success<T>(val data: T) : ApiResult<T>()
    data class Error(val code: Int, val message: String) : ApiResult<Nothing>()
    data class NetworkError(val exception: IOException) : ApiResult<Nothing>()
}

suspend fun <T> safeCall(call: suspend () -> Response<T>): ApiResult<T> {
    return try {
        val response = call()
        if (response.isSuccessful) {
            ApiResult.Success(response.body()!!)
        } else {
            ApiResult.Error(
                code = response.code(),
                message = response.errorBody()?.string() ?: "Unknown error"
            )
        }
    } catch (e: IOException) {
        ApiResult.NetworkError(e)
    }
}

Auth Token Injection

Do not manually add the Authorization header to every call. Use an OkHttp interceptor instead:

class AuthInterceptor(private val tokenProvider: () -> String) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer ${tokenProvider()}")
            .build()
        return chain.proceed(request)
    }
}

val client = OkHttpClient.Builder()
    .addInterceptor(authInterceptor)
    .build()

This keeps your service interface clean and ensures every request gets the token.

Retry with Exponential Backoff

Retrofit does not retry by default. Combine OkHttp interceptors with coroutines:

suspend fun <T> retryWithBackoff(
    retries: Int = 3,
    initialDelayMs: Long = 1000,
    factor: Double = 2.0,
    block: suspend () -> Response<T>
): Response<T> {
    var currentDelay = initialDelayMs
    repeat(retries - 1) { attempt ->
        try {
            val response = block()
            if (response.isSuccessful) return response
            if (response.code() != 429) return response
        } catch (e: IOException) { /* retry */ }
        delay(currentDelay)
        currentDelay = (currentDelay * factor).toLong()
    }
    return block() // Last attempt
}

Base URL Handling

Trailing slash matters. https://api.example.com/v1 and https://api.example.com/v1/ behave differently:

  • @GET("users") with base https://api.example.com/v1 resolves to https://api.example.com/v1/users
  • @GET("/users") always resolves to https://api.example.com/users and ignores the base path

Always include a trailing slash in your base URL: https://api.example.com/v1/.

Quiz

1. What annotation maps a URL path segment in Retrofit?

Question 1 options

2. Which converter factory is needed to serialize Kotlin data classes to JSON in Retrofit?

Question 2 options

3. Why use a sealed class for API responses instead of raw Response<T>?

Question 3 options

4. What happens when a Retrofit method is annotated with @GET("/users") with a leading slash?

Question 4 options

Flashcards

Question

What is the difference between @Body and @Field in Retrofit?

Answer

@Body serializes a Kotlin object using the converter factory (usually JSON). @Field sends a form-encoded key-value pair and requires @FormUrlEncoded on the method.

Question

How does Retrofit create an implementation of a service interface?

Answer

Retrofit uses a dynamic proxy at runtime. The create() method generates an implementation that translates annotated method calls into HTTP requests via OkHttp.

Question

What is the purpose of a converter factory in Retrofit?

Answer

A converter factory transforms between HTTP bodies and Kotlin types. GsonConverterFactory deserializes JSON responses into data classes and serializes @Body parameters to JSON.

Question

Why should you prefer suspend functions in Retrofit service interfaces?

Answer

Suspend functions integrate with Kotlin coroutines, allowing non-blocking calls without callbacks. Retrofit 2.6+ supports them natively, simplifying async code significantly.

Revision Notes

Key Takeaways

  • 1. Retrofit generates HTTP client code from annotated Kotlin interfaces
  • 2. Use suspend functions for clean coroutine integration
  • 3. Wrap Response<T> in a sealed class to handle all failure modes
  • 4. Handle auth tokens via OkHttp interceptors, not per-method headers

Interview Tips

  • Explain how Retrofit generates implementations via dynamic proxy
  • Discuss the difference between @Path and @Query parameters
  • Describe how you would handle token refresh on 401 responses
  • Compare Retrofit with raw OkHttp and when you might use each

Cheat Sheet

Retrofit Cheat Sheet

Setup:

  • Define API as a Kotlin interface
  • Use @GET, @POST, @PUT, @PATCH, @DELETE for methods
  • Add GsonConverterFactory or MoshiConverterFactory
  • Create instance with Retrofit.Builder().baseUrl().build()

Key Annotations:

  • @Path: URL segment (users/{id})
  • @Query: Query parameter (?page=1)
  • @Body: Serialized request body (JSON)
  • @Field: Form-encoded field (with @FormUrlEncoded)
  • @Header: Dynamic request header
  • @Headers: Static request headers

Patterns:

  • Wrap responses in sealed class (Success/Error/NetworkError)
  • Inject auth tokens via OkHttp interceptor, not @Header
  • Use suspend functions, not Call
  • Always include trailing slash in base URL