Computer scienceProgramming languagesKotlinAdditional instrumentsAdditional librariesCommunications and Networks

Retrofit

10 minutes read

Nowadays, it is impossible to imagine applications working just locally. Information flows through the internet, and it is essential to know how to access it. One of the most important information exchange technologies is REST services.

In this topic, we will focus on how to configure and use Retrofit as a REST client and to consume a REST service.

REST services

A REST service is a web-based architectural style for designing networked applications. It stands for Representational State Transfer. In simple terms, it enables communication between different systems over the internet by using standard HTTP methods, such as GET, POST, PUT, and DELETE. RESTful services allow clients (such as web browsers or mobile apps) to request and manipulate data resources on a server in a stateless manner. These services are widely used in modern web development for building scalable and interoperable APIs.

As information exchange formats, JSON (JavaScript Object Notation) and XML (eXtensible Markup Language) are commonly used with REST services to facilitate data interchange within RESTful services. JSON is lightweight and human-readable, which makes it popular for web applications.

HTTP verbs

These HTTP verbs define the actions that can be performed on resources within the client-server communication model. The most important HTTP verbs that we can use are:

  • GET: Retrieves a resource from a server. It is used to request data and does not have any side effects on the server.
  • POST: Submits data to be processed by a server. It is commonly used to create new resources or send data to be processed and stored.
  • PUT: Updates an existing resource on the server. It replaces the entire resource with the new data provided.
  • DELETE: Removes a specified resource from the server. It is used to delete a resource permanently.
  • PATCH: Partially updates an existing resource on the server. Unlike PUT, it only modifies specific attributes of the resource instead of replacing the entire resource.
  • HEAD: Retrieves the headers of a resource without retrieving the actual content. It is often used to check the status or headers of a resource without transferring the entire payload.
  • OPTIONS: Retrieves the supported HTTP methods and other capabilities of a server. It is commonly used to determine the available actions for a resource.

HTTP status codes

These status codes are part of the HTTP protocol and provide information about the success, failure, or other conditions related to the client-server communication. The most important ones are:

  • 200 OK: The request was successful, and the server has returned the requested resource.
  • 201 Created: The request was successful, and a new resource was created as a result.
  • 204 No Content: The server successfully processed the request, but there is no content to return.
  • 400 Bad Request: The server cannot understand the request due to malformed syntax or invalid parameters.
  • 401 Unauthorized: The request requires user authentication. The client must provide valid credentials.
  • 403 Forbidden: The server understands the request, but the client is not allowed to access the requested resource.
  • 404 Not Found: The server cannot find the requested resource.
  • 409 Conflict: The request could not be completed due to a conflict with the current state of the target resource.
  • 500 Internal Server Error: The server encountered an unexpected condition that prevented it from fulfilling the request.
  • 502 Bad Gateway: The server acting as a gateway or proxy received an invalid response from an upstream server.
  • 503 Service Unavailable: The server is temporarily unable to handle the request due to maintenance or overload.

Retrofit

Retrofit is a popular type-safe HTTP client library for Android and Java/Kotlin applications. Retrofit is a high-level REST abstraction built on top of OkHttp. When used to call REST applications, it greatly simplifies API interactions by parsing requests and responses into POJOs. It simplifies the process of making network requests by providing a high-level API that seamlessly integrates with existing codebases. Retrofit allows developers to define the structure and behavior of RESTful API calls using annotations and interfaces. With Retrofit, you can declare the HTTP request methods, URL endpoints, request parameters, headers, and expected response types in a concise and intuitive manner. It automatically handles the conversion of JSON or XML responses into Java/Kotlin objects using built-in or custom converters. Retrofit also supports various authentication mechanisms and can be extended with interceptors for additional functionality like logging, caching, or error handling (they are responsibilities and features of OkHttp).

Thus, you can use Retrofit as a perfect client to perform your operations with a REST service.

Installing and configuring Retrofit

To install Retrofit, we need to include the main dependence to our Gradle file: implementation("com.squareup.retrofit2:retrofit:(insert latest version)"), and we need a converter to transform Request and Response to Kotlin objects. By default, Retrofit can only deserialize HTTP bodies into OkHttp's ResponseBody type, and it can only accept its RequestBody type for @Body. Converters can be added to support other types. We can use:

  • 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
  • Kotlin Serialization: com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter

To install Retrofit with Kotlin serialization using Gradle Kotlin DSL (.gradle.kts), follow these steps in your build.gradle.kts file:

plugins {
    kotlin("jvm") version "1.8.1"
    // To use Kotlin Serialization
    kotlin("plugin.serialization") version "1.8.1"
}

    // ....
dependencies {
    // ...
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0") 
    // Kotlin serialization Coverter
    implementation("com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:1.0.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
    // ...
}

Note: Sync your project with Gradle files by clicking on the "Sync Now" button or by going to "File" -> "Sync Project with Gradle Files".

Using Retrofit

We will explain the steps to use Retrofit with the following REST service: https://reqres.in/.

1. Create DTOs

We need DTOs or a special class to serialize and deserialize the Request and Responses.

import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

@Serializable
data class UserDto(
    val id: Long,
    @SerialName("first_name")
    val firstName: String,
    @SerialName("last_name")
    val lastName: String,
    val avatar: String? = null,
    val email: String? = null,
    @SerialName("created_at")
    val createdAt: String? = null,
)

@Serializable
class GetAllDto(
    val page: Int,
    @SerialName("per_page")
    val perPage: Int = 0,
    val total: Int? = 0,
    @SerialName("total_pages")
    val totalPages: Int = 0,
    val data: List<UserDto> = listOf(),
    val support: SupportDto,
)

@Serializable
class GetByIdDto(
    val data: UserDto,
    val support: SupportDto,
)


@Serializable
class SupportDto(
    val url: String,
    val text: String,
)

@Serializable
class TokenDto(
    val token: String
)

@Serializable
class UploadDto(
    val url: String,
)

@Serializable
data class LoginDto(
    var email: String,
    var password: String
)

2. Define REST operations

The next step is to create an interface where we define each operation using Retrofit annotations and indicating the Request and Response types. There are eight built-in annotations: HTTP, GET, POST, PUT, PATCH, DELETE, OPTIONS, and HEAD. The relative URL of the resource is specified in the annotation, and you can also specify query parameters in the URL. A corresponding parameter of the URL must be annotated with @Path using the same string. Query parameters can also be added with @Query. An object can be specified for use as an HTTP request body with the @Body annotation. A request header can be updated dynamically using the @Header annotation. A corresponding parameter must be provided to the @Header (Map can be used to form complex headers). Multipart requests are used when @Multipart is present on the method. Parts are declared using the @Part annotation. Form-encoded data is sent when @FormUrlEncoded is present on the method. Each key-value pair is annotated with @Field containing the name and the object providing the value. Each method returns a Response, which contains information about the server response. The actual data returned by the server can be accessed through the body() method of the Response object. But if your response is "No Content" (204), the returned object is Void.

interface RetroFitRest {
    @GET("api/users")
    suspend fun getAll(@Query("page") page: Int = 0, @Query("per_page") perPage: Int = 0): Response<GetAllDto>

    @GET("api/users/{id}")
    suspend fun getById(@Path("id") id: Long): Response<GetByIdDto>

    @POST("api/users")
    suspend fun create(@Body user: UserDto): Response<UserDto>

    @PUT("api/users/{id}")
    suspend fun update(@Path("id") id: Long, @Body user: UserDto): Response<UserDto>

    @PATCH("api/users/{id}")
    suspend fun upgrade(@Path("id") id: Long, @Body user: UserDto): Response<UserDto>

    @DELETE("api/users/{id}")
    suspend fun delete(@Path("id") id: Long): Response<Void>

    @POST("api/login")
    suspend fun login(@Body user: LoginDto): Response<TokenDto>

    @GET("api/users")
    suspend fun getAllWithToken(
        @Header("Authorization") token: String,
        @Query("page") page: Int = 0,
        @Query("per_page") perPage: Int = 0
    ): Response<GetAllDto>

    @Multipart
    @POST("api/upload")
    suspend fun uploadFile(
        @Part filePart: MultipartBody.Part,
        @Part("description") description: RequestBody
    ): Response<UploadDto>
}

NOTE: We apply a suspended function to use Retrofit in an asynchronous way with Kotlin coroutines (the preferred or recommended form). Otherwise, you can use it with Call instances, which can be executed either synchronously or asynchronously in Java/Kotlin.

3. Create Retrofit client

To use Kotlin serialization with Retrofit, you need to configure the Retrofit instance to use the serialization converter. Here's an example:

object ApiRest {
    private const val API_URL = "https://reqres.in/"
    private val contentType = MediaType.get("application/json")

    val client = Retrofit.Builder()
        .baseUrl(API_URL) // API base URL
        .addConverterFactory(Json.asConverterFactory(contentType)) // Use Kotlinx serialization converter
        .build() // Create Retrofit instance
        .create(RetroFitRest::class.java) // Create API interface
}

Performing operations with Retrofit

In the following example, we have a UserRepository class that utilizes the Retrofit configuration and the provided Retrofit interface RetroFitRest to perform various operations.

class RetrofitRepository {
    suspend fun findAll(page: Int, perPage: Int): List<UserDto> {
        ApiRest.client.getAll(page, perPage).let { response ->
            if (response.isSuccessful && response.body() != null) {
                return response.body()!!.data
            } else {
                throw RestException("Error ${response.code()} to get users  ${response.errorBody()}")
            }
        }
    }

    suspend fun findAllWithToken(token: String, page: Int, perPage: Int): List<UserDto> {
        ApiRest.client.getAllWithToken(token, page, perPage).let { response ->
            if (response.isSuccessful && response.body() != null) {
                return response.body()!!.data
            } else {
                throw RestException("Error ${response.code()} to get users ${response.errorBody()}")
            }
        }

    }

    suspend fun findById(id: Long): UserDto {
        ApiRest.client.getById(id).let { response ->
            if (response.isSuccessful && response.body() != null) {
                return response.body()!!.data
            } else {
                throw RestException("Error ${response.code()} to get user by id ${response.errorBody()}")
            }
        }
    }

    suspend fun save(entity: UserDto): UserDto {
        ApiRest.client.create(entity).let { response ->
            if (response.isSuccessful && response.body() != null) {
                return response.body()!!
            } else {
                throw RestException("Error ${response.code()} to create user ${response.errorBody()}")
            }
        }
    }

    suspend fun update(entity: UserDto): UserDto {
        ApiRest.client.update(entity.id, entity).let { response ->
            if (response.isSuccessful && response.body() != null) {
                return response.body()!!
            } else {
                throw RestException("Error ${response.code()} to update user ${response.errorBody()}")
            }
        }
    }

    suspend fun delete(entity: UserDto): UserDto {
        ApiRest.client.delete(entity.id).let { response ->
            if (response.isSuccessful) {
                return entity
            } else {
                throw RestException("Error ${response.code()} to delete user ${response.errorBody()}")
            }
        }
    }

    suspend fun login(login: LoginDto): String {
        ApiRest.client.login(login).let { response ->
            if (response.isSuccessful && response.body() != null) {
                return response.body()!!.token
            } else {
                throw RestException("Error ${response.code()} to login ${response.errorBody()}")
            }
        }
    }

    suspend fun uploadFile(file: File, description: String): String {
        val filePart = MultipartBody.Part.createFormData(
            "file",
            file.name,
            RequestBody.create(MultipartBody.FORM, file)
        )
        val descriptionPart = RequestBody.create(MultipartBody.FORM, description)

        ApiRest.client.uploadFile(filePart, descriptionPart).let { response ->
            if (response.isSuccessful && response.body() != null) {
                return response.body()!!.url
            } else {
                throw RestException("Error ${response.code()} to upload file ${response.errorBody()}")
            }
        }
    }
}

Conclusion

In this topic, we have learned how to use Retrofit to consume REST services using Kotlin serialization. Now, you can interchange information across different services and enrich your apps.

Now is the time to do some tasks to check what you have learned. Are you ready?

11 learners liked this piece of theory. 1 didn't like it. What about you?
Report a typo