- Published on
How to do X with Retrofit
- Authors
- Name
- Yair Mark
- @yairmark
Kotlin Specific
Using Suspend Functions
This is as simple as swaping out the usual Retrofit interface method definition with a suspend
and a normal response (i.e. no wrappers to the response object).
You need to be using Retrofit 2.6.0 or higher to be able to use this.
For example the following:
@GET("persons/{id_number}")
fun getPerson(@Path("id_number") idNumber: String, @Query("ages") ages: QueryParameterList<Int>? = null): Call<List<Person>>
Becomes:
@GET("persons/{id_number}")
suspend fun getPerson(@Path("id_number") idNumber: String, @Query("ages") ages: QueryParameterList<Int>? = null): List<Person>
Authentication
Basic Auth
You will need to create an interceptor as below and then wire it up in your Retrofit config.
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
class BasicAuthInterceptor constructor(user: String, password: String) : Interceptor {
private var credentials: String = Credentials.basic(user, password)
override fun intercept(chain: Interceptor.Chain): Response {
val request: Request = chain.request()
val authenticatedRequest: Request = request.newBuilder()
.header("Authorization", credentials).build()
return chain.proceed(authenticatedRequest)
}
}
Then configure it when you build your Retrofit service:
import com.fasterxml.jackson.annotation.JsonProperty
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.slf4j.LoggerFactory
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.PATCH
import retrofit2.http.POST
import retrofit2.http.Path
import retrofit2.http.Query
interface PersonService {
// ... service methods here as normal
companion object {
private val logger = LoggerFactory.getLogger(this::class.java)
fun instance(properties: PersonServiceProperties): PersonService {
logger.info("START & END :: instance - properties $properties")
return Retrofit.Builder()
.client(buildHttpClient(properties))
.baseUrl(properties.baseUrl)
.addConverterFactory(JacksonConverterFactory.create(JacksonConfiguration().objectMapperForPersonService()))
.build()
.create(PersonService::class.java)
}
private fun buildHttpClient(properties: PersonServiceProperties): OkHttpClient = if (properties.debug.toBoolean()) {
val logging: HttpLoggingInterceptor = HttpLoggingInterceptor().setLevel(level = HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder().addInterceptor(logging)
} else {
OkHttpClient.Builder()
}
.addInterceptor(BasicAuthInterceptor(user = properties.username, password = properties.password))
.build()
}
}
OAuth
This involves 3 pieces:
- Creating a class that extends
okhttp3.Authenticator
- Creating a service to hit the OAuth token endpoint
- Adding what you created in 2. combined with 1. as part of building up the service that actually needs the OAuth before each call
The authenticator looks as follows:
import okhttp3.Authenticator
import okhttp3.Credentials
import okhttp3.Request
import okhttp3.Response
import okhttp3.Route
// authService here is what we will build below for 2.
class TokenAuthenticator(private val properties: Properties, internal val authService: AuthService) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? {
val credential: String = Credentials.basic(username = properties.username, password = properties.password)
val currentHeaderAuthorization: String? = response.request.header(AuthService.HEADER_AUTHORIZATION)
val responseCount: Int = response.responseCount
if (currentHeaderAuthorization == credential || currentHeaderAuthorization != null || responseCount > 2) {
return null
}
val token: AuthTokenResponse = authService.refreshToken(credential)
return response.request.newBuilder()
.header(AuthService.HEADER_AUTHORIZATION, "${token.tokenType} ${token.accessToken}")
.header("Content-Type", "application/x-www-form-urlencoded")
.build()
}
private fun AuthService.refreshToken(credential: String): AuthTokenResponse = this.getAuthenticationToken(
authorization = credential,
contentType = "application/x-www-form-urlencoded;charset=UTF-8",
params = hashMapOf(
"grant_type" to "client_credentials"
)
).execute().body() ?: throw RuntimeException("Null returned when trying to authenticate")
}
The service to hit the OAuth endpoint looks as follows:
import io.github.resilience4j.retrofit.CircuitBreakerCallAdapter
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.slf4j.LoggerFactory
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.FieldMap
import retrofit2.http.FormUrlEncoded
import retrofit2.http.Header
import retrofit2.http.POST
interface AuthService {
@FormUrlEncoded
@POST("oauth/token")
fun getAuthenticationToken(
@Header(HEADER_AUTHORIZATION) authorization: String,
@Header(CONTENT_TYPE) contentType: String,
@FieldMap params: HashMap<String, String>
): retrofit2.Call<AuthTokenResponse>
companion object {
private val logger = LoggerFactory.getLogger(AuthService::class.java)
private const val serviceName = "SomeAPIS[Auth]"
const val HEADER_AUTHORIZATION: String = "Authorization"
const val CONTENT_TYPE: String = "Content-Type"
fun instance(properties: Properties): AuthService {
logger.info("START & END :: instance")
return Retrofit.Builder().let { builder ->
logger.info("START & END :: instance - properties $properties")
builder.client(httpClientBuilder(properties).build())
}.baseUrl(properties.environment.baseURL)
.addConverterFactory(JacksonConverterFactory.create(JacksonConfiguration.lowerSnakeCaseConfiguration))
.build().create(AuthService::class.java)
}
private fun httpClientBuilder(properties: Properties): OkHttpClient.Builder {
val httpClient = if (properties.mustLogAPI) {
val logging: HttpLoggingInterceptor = HttpLoggingInterceptor().setLevel(level = HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder().addInterceptor(logging)
} else {
OkHttpClient.Builder()
}
return httpClient
}
}
}
Finally we tie it all together by using it in the service that needs OAuth before calling an endpoint:
import io.github.resilience4j.retrofit.CircuitBreakerCallAdapter
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.slf4j.LoggerFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import java.time.LocalDateTime
interface MyService {
// service methods over here as usual
companion object {
private val logger = LoggerFactory.getLogger(MyService::class.java)
private const val serviceName = "MyService[General]"
fun instance(properties: Properties, authService: AuthService): RealPayService {
logger.info("START & END :: instance - properties $properties")
return Retrofit.Builder()
.client(buildHttpClient(properties, authService))
.baseUrl(properties.environment.baseURL)
.addConverterFactory(JacksonConverterFactory.create(JacksonConfiguration.upperCamelCaseConfiguration))
.addConverterFactory(QueryConverterFactory.create())
.build()
.create(RealPayService::class.java)
}
private fun buildHttpClient(properties: Properties, authService: AuthService): OkHttpClient = if (properties.mustLogAPI) {
val logging: HttpLoggingInterceptor = HttpLoggingInterceptor().setLevel(level = HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder().addInterceptor(logging)
} else {
OkHttpClient.Builder()
}
.authenticator(TokenAuthenticator(properties = properties, authService = authService))
.build()
}
}
Interceptors
Log All Requests and Responses
This can be done by adding a HttpLoggingInterceptor
when you build up the HttpClient for your service
This snippet is what does it but the entire code block below that will show you the full context
val logging: HttpLoggingInterceptor = HttpLoggingInterceptor().setLevel(level = HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder().addInterceptor(logging)
Full snippet:
import io.github.resilience4j.retrofit.CircuitBreakerCallAdapter
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import org.slf4j.LoggerFactory
import retrofit2.Call
import retrofit2.Retrofit
import retrofit2.converter.jackson.JacksonConverterFactory
import retrofit2.http.Body
import retrofit2.http.GET
import retrofit2.http.POST
import retrofit2.http.PUT
import retrofit2.http.Path
import retrofit2.http.Query
import java.time.LocalDateTime
interface MyService {
// service methods over here as usual
companion object {
private val logger = LoggerFactory.getLogger(MyService::class.java)
private const val serviceName = "MyService[General]"
fun instance(properties: Properties, authService: AuthService): RealPayService {
logger.info("START & END :: instance - properties $properties")
return Retrofit.Builder()
.client(buildHttpClient(properties, authService))
.baseUrl(properties.environment.baseURL)
.addConverterFactory(JacksonConverterFactory.create(JacksonConfiguration.upperCamelCaseConfiguration))
.addConverterFactory(QueryConverterFactory.create())
.build()
.create(RealPayService::class.java)
}
private fun buildHttpClient(properties: Properties, authService: AuthService): OkHttpClient = if (properties.mustLogAPI) {
val logging: HttpLoggingInterceptor = HttpLoggingInterceptor().setLevel(level = HttpLoggingInterceptor.Level.BODY)
OkHttpClient.Builder().addInterceptor(logging)
} else {
OkHttpClient.Builder()
}
.authenticator(TokenAuthenticator(properties = properties, authService = authService))
.build()
}
}
Building up the URL
Optional Query Parameters
This is as simple as making the query parameter nullable. If you are working in Kotlin you can make it telescope to a null.
@GET("persons/{id_number}")
suspend fun getPerson(@Path("id_number") idNumber: String, @Query("ages") ages: QueryParameterList<Int>? = null): List<Person>
In the above ages
is an optional query parameter. When null is assigned to it, Retrofit will not add it to the query parameter list
Comma Separated List in Query Parameters
This relies on the fact that Retrofit:
- Allows complex objects to be used as query parameters
- Uses the toString of an object to convert the complex object to a string for the query
This also uses the following custom class to more easily facilitate this parameter list conversion:
data class QueryParameterList<T>(
private val separator: Char = ',',
val values: List<T>
) {
override fun toString(): String = values.joinToString(separator = separator.toString())
}
Then finally to use this simply define it as follows:
@GET("persons/{id_number}")
suspend fun getPerson(@Path("id_number") idNumber: String, @Query("ages") ages: QueryParameterList<Int>? = null): List<Person>
In the above ages
is an optional query parameter. When null is assigned to it, Retrofit will not add it to the query parameter list