Codersee

Retrofit With Kotlin- The Ultimate Guide

A featured image for category: Kotlin

1. Introduction

This time, I would like to show you how to use Retrofit 2 with Kotlin. I will walk you step by step through its features, capabilities and a few obstacles you may encounter when using it.

Let’s start by answering the question: what exactly the Retrofit is? It is a type-safe HTTP client for Android and Java. If you are looking for a library, which will help you to integrate external API in your mobile application, or back-end service written in Java or Kotlin, then Retrofit is definitely worth a try.

2. Imports

It’s worth mentioning, that Retrofit requires at minimum Java 8+ or Android API 21+, so it won’t be compatible with legacy codebase.

Moreover, depending on when you are reading this article, the latest stable version can be different and I recommend using the latest one. In my examples, I will be using version: 2.9.0 .

2.1. Maven

If you are working with Maven, then you can add it to your project with the following lines:

<dependency>
  <groupId>com.squareup.retrofit2</groupId>
  <artifactId>retrofit</artifactId>
  <version>(VERSION)</version>
</dependency>

2.2. Gradle

Alternatively, we can use Gradle to fetch the library:

// build.gradle
implementation 'com.squareup.retrofit2:retrofit:<VERSION>'

// build.gradle.kts
implementation("com.squareup.retrofit2:retrofit:<VERSION>")

2.3. Serialization & Deserialization

By default, Retrofit allows us to work only with OkHttp’s RequestBody and ResponseBody objects. Although sometimes it might be sufficient, in most cases we would like to make use of some serialization library when dealing with payloads.

Thankfully, we can achieve that pretty easily by importing desired Converter to our project:

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • JAXB: com.squareup.retrofit2:converter-jaxb
  • Scalars: com.squareup.retrofit2:converter-scalars

For the purposes of this tutorial, I’ve picked the Jackson library, so we need to add the following to build.gradle.kts file:

implementation("com.squareup.retrofit2:converter-jackson:2.9.0")

3. Retrofit Configuration With Kotlin

As the next step, let’s see how can we configure Retrofit to work with Kotlin.

3.1. Add DTO

Firstly, let’s add an example user DTO to our project:

data class User(
  @JsonProperty("id") val id: Long,
  @JsonProperty("name") val name: String,
  @JsonProperty("age") val age: Int
)

As we can see, this simple class expects the JSON returned by the external API to be in the following format:

{
  "id": 1,
  "name": "Name-1",
  "age": 21
}

3.2. Implement RetrofitClient

Following, let’s add create a RetrofitClient object:

object RetrofitClient {

  private const val BASE_URL = "http://localhost:8090/v1/"

  fun getClient(): Retrofit =
    Retrofit.Builder()
      .baseUrl(BASE_URL)
      .addConverterFactory(JacksonConverterFactory.create())
      .build()
}

The above code is basically responsible for the following:

  • setting the base URL for external API
  • registering converter factory for objects serialization and deserialization
  • creating Retrofit instance

It’s worth mentioning that by default the OkHttpClient will be used underneath, as a factory for calls. Additionally we have to keep in mind that baseUrl has to end in /. Otherwise, the build will fail with IllegalArgumentException.

3.3. Add OkHttpClient Interceptor

As the next step let’s take a look on how to add interceptors to our requests.

If you are wondering why would we need it, then the answer is pretty simple. Oftentimes, when working with external APIs we want to perform some repeatable actions, like:

  • adding authorization headers
  • logging
  • error handling etc.

And to avoid repeating the same code across the whole app, we can simply add an interceptor, which will affect our calls.

With that being said, let’s implement a simple RequestInterceptor:

object RequestInterceptor : Interceptor {
  override fun intercept(chain: Interceptor.Chain): Response {
    val request = chain.request()
    println("Outgoing request to ${request.url()}")
    return chain.proceed(request)
  }
}

As we can see, the above code will print the information about outgoing requests URLs to the output. Later in the article, we will see how to add more interceptors.

But for now, let’s modify the RetrofitClient to make use of a new interceptor:

val okHttpClient = OkHttpClient()
  .newBuilder()
  .addInterceptor(RequestInterceptor)
  .build()

fun getClient(): Retrofit =
  Retrofit.Builder()
    .client(okHttpClient)
    .baseUrl(BASE_URL)
    .addConverterFactory(JacksonConverterFactory.create())
    .build()

4. Handle Responses

As the next step, let’s see how to handle Retrofit responses in our Kotlin codebase.

Let’s start with adding the UserApi interface:

interface UserApi {
  @GET("users")
  fun getUsers(): Call<List<User>>
}

As can be seen, the above function will be used to perform GET requests to http://localhost:8090/v1/users endpoint. In the next chapter we will take a closer look at all supported HTTP methods, but for now, let’s discuss the Call interface.

A Call is basically an invocation of a Retrofit method that sends a request to a web server and returns a response. It can be used synchronously with the execute or asynchronously with enqueue methods. Additionally, each call can be cancelled at any time with cancel.

In this article, we will focus on synchronous calls. Nevertheless, if you would like to learn how to work with Retrofit and Kotlin coroutines, please let me know in the comments section. I will be more than happy to make a continuation for this topic.

4.1. Response Information

With that being said, let’s add a UserService and make use of our configuration:

class UserService {

  private val retrofit = RetrofitClient.getClient()
  private val userApi = retrofit.create(UserApi::class.java)

}

Following, let’s see what information about the response can we obtain with Retrofit:

fun successfulUsersResponse() {
  val usersResponse = userApi.getUsers()
    .execute()

  val successful = usersResponse.isSuccessful
  val httpStatusCode = usersResponse.code()
  val httpStatusMessage = usersResponse.message()

}

Basically, the execute() method returns the Response<T>, which lets us obtain info about the… response 🙂

Let’s have a close look at each line:

val successful = usersResponse.isSuccessful

The isSuccessful returns true, if the HTTP code is in the range [200…300]. It is a really good choice when we don’t care about the specific codes.

val httpStatusCode = usersResponse.code()

On the other hand, the code() method returns the exact Integer value of the response HTTP status code.

val httpStatusMessage = usersResponse.message()

Finally, the message(), might be useful, if we would like to get the HTTP status message, like OK.

4.2. Successful Response Body

Following, let’s see how easily we can obtain the response body:

val body: List<User>? = usersResponse.body()

Nevertheless, please keep in mind that Retrofit body() method may return null, so we should handle it properly with Kotlin.

4.3. Retrofit Error Body

Life would be much easier, if there were no errors.

Nevertheless, they happen, and it’s better to get more information about them:

val errorBody: ResponseBody? = usersResponse.errorBody()

The rule is pretty simple here: when the response is successful, the body is populated. Oherwise, the errorBody field is not null. It’s worth mentioning here that Retrofit errorBody() method is not generic and we have to handle payload manually.

Let’s see how can we achieve that with Jackson library. Firstly, let’s add the ErrorResponse data class:

data class ErrorResponse(
  @JsonProperty("message") val message: String
)

Following, let’s use the ObjectMapper:

val errorBody: ResponseBody? = usersResponse.errorBody()

val mapper = ObjectMapper()

val mappedBody: ErrorResponse? = errorBody?.let { notNullErrorBody ->
  mapper.readValue(notNullErrorBody.toString(), ErrorResponse::class.java)
}

This way, when Retrofit populates the errorBody property, we use Kotlin’s let function to deserialize payload into the ErrorResponse instance.

4.4. Read Response Headers

Finally, let’s take a look at response headers, because sometimes they can contain useful information:

val headers = usersResponse.headers()
val customHeaderValue = headers["custom-header"]

As we can see, with these two lines we can simply obtain the value of the custom-header returned from external API.

5. HTTP Methods

So far, we’ve learned how to handle various responses, but GET is not the only option, so let’s have a look at other possibilities.

Basically, Retrofit comes with 8 built-in annotations: HTTP, GET, POST, PUT, PATCH, DELETE, OPTIONS and HEAD. Although the choice will depend on the endpoint we query, I would like to take a closer look at @HEAD and @HTTP annotations.

5.1. Retrofit @HEAD

Let’s start with the @HEAD annotation:

@HEAD("users")
fun headUsers(): Call<Void>

As we can see, the return type here is Void– and it must be always be. Otherwise, Retrofit will throw IllegalArgumentException with: HEAD method must use Void as response type message.

5.2. Retrofit @HTTP and @DELETE

The @HTTP annotation is pretty interesting, as well. It lets us use a custom HTTP verb for a request.

Let’s see an example:

@GET("users")
fun getUsers(): Call<List<User>>

@HTTP(method = "GET", path = "users")
fun httpUsers(): Call<List<User>>

Basically, both functions work exactly the same and in real-life scenarios we will hardly ever use the second option. Given that, why should we care about it?

Well, let’s see the following snippet:

@DELETE("users")
fun failingDeleteUser(@Body user: User): Call<User>

As the name of the function suggests, it will fail. And to be more specific, it will fail with the following message: Non-body HTTP method cannot contain @Body.

In such a case, the @HTTP annotation allows us to set hasBody flag, which we can use to perform the DELETE request with payload:

@HTTP(method = "DELETE", path = "users", hasBody = true)
fun workingDeleteUser(@Body user: User): Call<User>

6. URL Manipulation

Until now, we saw how to use a hard-coded URLs and sometimes it will be sufficient. Nevertheless, in real-life scenarios we will have to specify additional query parameters, or path variables sooner or later.

6.1. Use Absolute Path

As the first step, let’s see how can we instruct Retrofit to use a custom, absolute path in our Kotlin interface:

@GET("http://localhost:8090/v3/users")
fun getUsersV3(): Call<List<User>>

This might be really helpful, when we would like to query another API, or just another version of the same provider without the need to set up next interface. Just like above handler will perform a GET request to V3 endpoint.

6.2. Dynamic URL With @Url

On the other hand, Retrofit has the possibility to specify the URL on method invocation:

@GET
fun getUsersDynamic(@Url url: String): Call<List<User>>

This way, we have the possibility to specify desired URL as an argument. Nevertheles, it’s a good time to have a look on how URLs are resolved in Retrofit:

  • userApi.getUsersDynamic(“users”) – will be resolved to http://localhost:8090/v1/users
  • userApi.getUsersDynamic(“/users”) – will be treated as http://localhost:8090/users
  • userApi.getUsersDynamic(“http://localhost:8090/v3/users”) – just like in the previous example, will use the provided, absolute path

6.3. Use Path Variable

Following, let’s see how to make use of replacement blocks:

@GET("users/{userId}")
fun getUserById(@Path("userId") userId: Int): Call<User>

As we can see, we can specify the user id when invoking the function. So the following call: userApi.getUserById(99), will result in GET request to http://localhost:8090/v1/users/99.

6.4. Add Query Parameters

Another common way of exchanging information are query parameters:

@GET("users?sort_order=asc")
fun getUsersStaticQueryParam(): Call<List<User>>

As can be seen, we use a static sort_order query parameter. Although I discourage this approach I wanted to show that it is possible when working with Retrofit.

To get a better control over the sent parameters, we can use a @Query annotation:

@GET("users")
fun getUsersDynamicQueryParam(@Query("sort_order") order: String): Call<List<User>>

This way, our function becomes much more generic. Calling it with the “desc” argument, will result in sort_order=desc in our URL.

As the last one, let’s see how can we add multiple parameters with @QueryMap:

@GET("users")
fun getUsersDynamicQueryMap(@QueryMap parameters: Map<String, String>): Call<List<User>>

This way, when the API requires multiple query parameters, we can encapsulate them in a map. Without that we would end up with a function taking plenty of arguments, which would affect code readability.

So, the following code:

userApi.getUsersDynamicQueryMap(
  mapOf(
    "order" to "asc",
    "name" to "some"
  )
)

Will be translated to: http://localhost:8090/v1/users?order=asc&name=some

7. Request Headers

Headers are inseparable part of every REST communication. Oftentimes, we will have to send additional information, like Content-Type, or Authorization. In this chapter we will learn a few possibilities Retrofit gives us to work with them.

7.1. Static Headers

Let’s start with a single header:

@Headers("User-Agent: codersee-application")
@GET("users")
fun getUsersSingleStaticHeader(): Call<List<User>>

Similarly, we can append multiple headers to the request:

@Headers(
  value = [
    "User-Agent: codersee-application",
    "Custom-Header: my-custom-header"
])
@GET("users")
fun getUsersMultipleStaticHeaders(): Call<List<User>>

7.2. Dynamic Headers

Although the two above will be useful in some cases, oftentimes we will need to pass a dynamic value for a header, like Authorization for instance. To achieve that, we can use a @Header annotation:

@GET("users")
fun getUsersDynamicHeader(@Header("Authorization") token: String): Call<List<User>>

Just like with @Query annotation, Retrofit allows us to pass multiple headers in a Map form with @HeaderMap:

@GET("users")
fun getUsersHeaderMap(@HeaderMap headers: Map<String, String>): Call<List<User>>

7.3. Authorization Interceptor

As the next step, I would like to show you how to make life easier and extract logic responsible for tokens generation to Retrofit config. This is way, each request to the given API will contain a generated token and we won’t need to add it each time.

Let’s start with adding a Kotlin object called AuthorizationInterceptor:

object AuthorizationInterceptor : Interceptor {
  override fun intercept(chain: Interceptor.Chain): Response {
    val requestWithHeader = chain.request()
      .newBuilder()
      .header(
        "Authorization", UUID.randomUUID().toString()
      ).build()
    return chain.proceed(requestWithHeader)
  }
}

Just like in the chapter number 3, we extend the Interceptor interface, but this time we additionally call a newBuilder() on the request instance. With that being done, we can modify its behavior, like add headers or tags (and I highly encourage you to check all the possible options). In your project, the UUID.randomUUID().toString() would be replaced with a call to another service, or logic responsible for generating real tokens.

As the last step, we have to add it to the OkHttpClient:

val okHttpClient = OkHttpClient()
  .newBuilder()
  .addInterceptor(AuthorizationInterceptor)
  .addInterceptor(RequestInterceptor)
  .build()

This way, each request will be enhanced with the Authorization header.

8. Request Body

Finally, we can learn how to attach a payload to Retrofit requests.

8.1. Send Object as JSON

Let’s start with the most popular way of sending data- a JSON payload:

@POST("users")
fun postUsersWithPayload(@Body user: User): Call<User>

With the @Body annotation, we can pass a User instance, which will be then serialized using configured Retrofit converter (Jackson in our case).

8.2. Retrofit URL-Encoded Form

On the other hand, if we would like to send data in a URL-Encoded Form, we can use the @FormUrlEncoded along with @Field:

@FormUrlEncoded
@POST("users")
fun postUsersFormUrlEncoded(@Field("field_one") fieldOne: String): Call<User>

8.3. Multipart

Lastly, to send the data as a HTTP multipart request, we can use @Multipart on a function with @Part on each part send to the external REST API:

@Multipart
@POST("users")
fun postUsersMultipart(@Part("something") partOne: RequestBody): Call<User>

9. Retrofit With Kotlin Summary

And that would be all for this article on Retrofit with Kotlin. We’ve learned together almost everything you will need to know when working on your very own project. As always, you can find the source code in this GitHub Repository.

On the other hand, if you would like to combine this knowledge and learn how to create back-end services with Kotlin, you will find a step-by step tutorials in the Ktor category.

I hope you enjoyed this article and will be really happy if you would like to give me a feedback in the comments section.

4 Responses

  1. Nice article! Thank you!
    Could you please add a tutorial to how to fire up local server for testing retrofit topic?
    Best regards,
    I appreciate

    1. Hello! Glad to hear that.
      Hmmm, not sure what exactly do you mean by local server. If you would like to see how to create REST API with Kotlin, then please check out Ktor category: https://codersee.com/articles/
      Also, a Mockoon is pretty good tool to mock API, so probably will create some video about it in the future 🙂

Leave a Reply

Your email address will not be published.

Categories

Author

Piotr Wolak

Piotr Wolak

Founder Of Codersee

Join Newsletter And Get 2 FREE EBOOKS

Image shows the covers of free ebooks accessible for newsletter subscribers.

Join the FREE weekly newsletter and get two free eBooks:

Image shows the covers of free ebooks accessible for newsletter subscribers.

You may opt out any time. Terms of Use and Privacy Policy.

To make Codersee work, we log user data. By using our site, you agree to our Privacy Policy and Terms of Use.