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 basehttps://api.example.com/v1resolves tohttps://api.example.com/v1/users@GET("/users")always resolves tohttps://api.example.com/usersand 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?
2. Which converter factory is needed to serialize Kotlin data classes to JSON in Retrofit?
3. Why use a sealed class for API responses instead of raw Response<T>?
4. What happens when a Retrofit method is annotated with @GET("/users") with a leading slash?
Flashcards
Question
What is the difference between @Body and @Field in Retrofit?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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?
Click to reveal answer
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