Android Networking : Retrofit at a Glance

Romman SabbirRomman Sabbir
9 min read

If get to know something new by reading my articles, don't forget to endorse me on LinkedIn

In the realm of Android development, Retrofit has emerged as a go-to library for handling API communication. Its simplicity, flexibility, and powerful features make it an ideal choice for interacting with web services. In this blog post, we will dive deep into Retrofit and explore how to use it for different HTTP methods, including GET, POST, PUT, PATCH, and DELETE. Additionally, we'll discover the concept of mock responses and learn how to use them with Retrofit. All examples will be written in Kotlin.

To get started with Retrofit, add the following dependency to your project's build.gradle file:

implementation 'com.squareup.retrofit2:retrofit:2.9.0'

To enable support for Gson serialization/deserialization also includes the Gson converter dependency:

implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

Various HTTP methods supported by Retrofit

  • GET Request: A GET request s used to retrieve data from a server. Example of how to perform a GET request using Retrofit:
interface UserApiService {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: String): Response<User>
}

val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

val apiService = retrofit.create(UserApiService::class.java)

// Making the GET request
val response = apiService.getUser(999)
if (response.isSuccessful) {
    val user = response.body()
    // Handle the user data
} else {
    // Handle the error
}
  • POST Request: A POST request is used to send data to a server. Example of how to perform a POST request using Retrofit:
interface UserApiService{
    @POST("users")
    suspend fun createUser(@Body user: User): Response<User>
}

val user = User("Romman Sabbir", "rommansabbir@gmail.com")

// Making the POST request
val response = apiService.createUser(user)
if (response.isSuccessful) {
    val createdUser = response.body()
    // Handle the created user data
} else {
    // Handle the error
}
  • PUT Request: A PUT request is used to update existing resources on the server. Example of how to perform a PUT request using Retrofit:
interface UserApiService{
    @PUT("users/{id}")
    suspend fun updateUser(@Path("id") userId: String, @Body updatedUser: User): Response<User>
}

val updatedUser = User("Romman Sabbir", "rommansabbir@gmail.com")

// Making the PUT request
val response = apiService.updateUser(999, updatedUser)
if (response.isSuccessful) {
    val updatedUser = response.body()
    // Handle the updated user data
} else {
    // Handle the error
}
  • PATCH Request: A PATCH request is similar to a PUT request, but it is used to perform partial updates on resources. Example of how to perform a PATCH request using Retrofit:
interface UserApiService{
    @PATCH("users/{id}")
    suspend fun updateUser(@Path("id") userId: String, @Body updatedFields: Map<String, String>): Response<User>
}

val updatedFields = mapOf("email" to "rommansabbir@gmail..com")

// Making the PATCH request
val response = apiService.updateUser(999, updatedFields)
if (response.isSuccessful) {
    val updatedUser = response.body()
    // Handle the updated user data
} else {
    // Handle the error
}
  • DELETE Request: A DELETE request is used to delete resources from the server. Here's an example of how to perform a DELETE request using Retrofit:
interface UserApiService{
    @DELETE("users/{id}")
    suspend fun deleteUser(@Path("id") userId: String): Response<Unit>
}

// Making the DELETE request
val response = apiService.deleteUser(999)
if (response.isSuccessful) {
    // Handle the successful deletion
} else {
    // Handle the error
}

Different Kinds of POST Method w/ Retrofit

Retrofit supports various types of POST methods to send data to a server. Let's explore different types of POST methods and how to implement them using Kotlin with Retrofit.

  • Form URL-encoded POST request: Form URL-encoded requests are commonly used when sending data as key-value pairs, similar to how HTML forms are submitted. Here's an example of how to implement a form URL-encoded POST request using Retrofit:

Define the API service interface:

interface ApiService {
    @FormUrlEncoded
    @POST("endpoint")
    fun sendData(
        @Field("param1") param1: String,
        @Field("param2") param2: String
    ): Call<ResponseBody>
}

Make the POST request:

val call = apiService.sendData("value1", "value2")
call.enqueue(object : Callback<ResponseBody> {
    override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
        if (response.isSuccessful) {
            // Request successful
        } else {
            // Handle error
        }
    }

    override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
        // Handle failure
    }
})
  • JSON POST request: JSON POST requests are commonly used when sending complex data structures or objects as JSON payloads. Here's an example of how to implement a JSON POST request using Retrofit:

Define the API service interface:

interface ApiService {
    @Headers("Content-Type: application/json")
    @POST("endpoint")
    fun sendData(@Body requestBody: RequestBody): Call<ResponseBody>
}

We specify the "Content-Type" header as "application/json" using the @Headers annotation. The request body is represented by the @Body annotation, and we pass a RequestBody object.

Make the POST request:

val requestBody = RequestBody.create(MediaType.parse("application/json"), json)
val call = apiService.sendData(requestBody)
call.enqueue(object : Callback<ResponseBody> {
    override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
        if (response.isSuccessful) {
            // Request successful
        } else {
            // Handle error
        }
    }

    override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
        // Handle failure
    }
})
  • Multipart POST request (file upload): Multipart requests are used when uploading files to a server. We will explore Multipart POST below.

Post Image, Audio, Video w/ Retrofit

  • Create an interface that defines the API endpoints for uploading media files. Assume you have a RESTful API with an endpoint /upload that accepts a media file. Create a new Kotlin file, e.g., MediaService.kt, and define the service interface as follows:
interface MediaService {
    @Multipart
    @POST("/upload")
    fun uploadMedia(
        @Part("image\"; filename=\"image.jpg\"") image: RequestBody?,
        @Part("audio\"; filename=\"audio.mp3\"") audio: RequestBody?,
        @Part("video\"; filename=\"video.mp4\"") video: RequestBody?
    ): Call<ResponseBody>
}

We have a single API endpoint /upload that accepts three different media files: image, audio, and video. Each file is represented by a RequestBody object annotated with the @Part annotation. The filename parameter specifies the desired filename for each file.

  • Prepare the media files for upload

Before uploading the media files, we need to prepare them for upload. Assuming you have the file paths or URIs for each file, you can create RequestBody objects from them. Here's an example of how to do it:

val imageFile = File(imageFilePath)
val imageRequestBody = imageFile.asRequestBody("image/*".toMediaTypeOrNull())

val audioFile = File(audioFilePath)
val audioRequestBody = audioFile.asRequestBody("audio/*".toMediaTypeOrNull())

val videoFile = File(videoFilePath)
val videoRequestBody = videoFile.asRequestBody("video/*".toMediaTypeOrNull())

imageFilePath, audioFilePath, and videoFilePath represent the paths to the respective media files. We create RequestBody objects using the asRequestBody extension function, specifying the media type based on the file extension.

  • Upload the media files

Now that everything is set up, you can upload the media files using the Retrofit service interface. Make the API call by invoking the uploadMedia method and passing the respective RequestBody objects as parameters:

val call = mediaService.uploadMedia(imageRequestBody, audioRequestBody, videoRequestBody)
call.enqueue(object : Callback<ResponseBody> {
    override fun onResponse(call: Call<ResponseBody>, response: Response<ResponseBody>) {
        if (response.isSuccessful) {
            // Media upload successful
        } else {
            // Handle error
        }
    }

    override fun onFailure(call: Call<ResponseBody>, t: Throwable) {
        // Handle failure
    }
})

Post File w/ Retrofit

  • Create a service interface defining the API endpoints for file upload:
interface FileUploadService {
    @Multipart
    @POST("upload")
    suspend fun uploadFile(
        @Part file: MultipartBody.Part
    ): Response<YourResponseModel>
}
  • Create the RequestBody for your file:
val file = File("path_to_your_file")
val requestFile = RequestBody.create(MediaType.parse("multipart/form-data"), file)
val body = MultipartBody.Part.createFormData("file", file.name, requestFile)
  • Make the API call to upload the file:
try {
    val response = service.uploadFile(body)
    if (response.isSuccessful) {
        // Handle success
    } else {
        // Handle error
    }
} catch (e: Exception) {
    // Handle exception
}

File Upload Progress

Add OkHttp dependency:

implementation("com.squareup.okhttp3:okhttp:4.10.0")

To track the progress of the file upload, we can use an OkHttpClient instance with a custom Interceptor that implements the ProgressListener interface. This listener receives progress updates during the upload process. Here's an example of how we can implement it:

class ProgressInterceptor(private val progressListener: ProgressListener) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val requestBuilder = originalRequest.newBuilder()

        val requestBody = originalRequest.body
        if (requestBody != null) {
            val progressRequestBody = ProgressRequestBody(requestBody, progressListener)
            requestBuilder.method(originalRequest.method, progressRequestBody)
        }

        val request = requestBuilder.build()
        return chain.proceed(request)
    }
}

interface ProgressListener {
    fun onProgress(progress: Int)
}

The ProgressInterceptor intercepts the network request and wraps the request body with a ProgressRequestBody. The ProgressListener interface provides a callback to track the progress of the upload.

To use the ProgressInterceptor, modify the creation of the OkHttpClient instance as follows:

val progressInterceptor = ProgressInterceptor(object : ProgressListener {
    override fun onProgress(progress: Int) {
        // Update the progress here
    }
})

val httpClient = OkHttpClient.Builder()
    .addInterceptor(progressInterceptor)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl("http://example.com/")
    .client(httpClient)
    .build()

The ProgressInterceptor is added to the OkHttpClient instance using the addInterceptor method. This allows the interceptor to track the progress of the file upload. The onProgress method in the ProgressListener interface is called with the progress value during the upload process, allowing us to update the progress as needed.


Using Mock Responses in Retrofit w/ MockWebServer

Mock responses are incredibly useful during testing and development, allowing you to simulate server responses without relying on an actual backend. Retrofit provides the MockWebServer class for creating a local HTTP server that responds with pre-defined mock responses. Here's how you can use mock responses with each HTTP method in Retrofit:

  • Mock GET Request:
val server = MockWebServer()
server.enqueue(MockResponse().setBody("This is a simple mock response."))

// Start the server and make the request
server.start()
val baseUrl = server.url("/")
val retrofit = Retrofit.Builder()
    .baseUrl(baseUrl)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

// Define the API service
interface MockUserApiService {
    @GET("data")
    suspend fun getData(): Response<String>
}

val apiService = retrofit.create(MockUserApiService::class.java)

// Make the GET request
val response = apiService.getData()
if (response.isSuccessful) {
    val responseData = response.body()
    // Handle the mock response data
} else {
    // Handle the error
}

// Shutdown the server
server.shutdown()
  • Mock POST, PUT, PATCH, and DELETE Requests: Mocking POST, PUT, PATCH, and DELETE requests follow a similar pattern. You need to define the respective annotations in the API service interface and enqueue the desired mock response in the MockWebServer.
// Similar steps as the GET request, just replace the annotation in the API service interface
interface MockApiService {
    @POST("data")
    suspend fun postData(@Body requestBody: RequestBody): Response<String>
    // Other mock methods here
}

Mock Responses in Retrofit from Android Assets

Mock responses are invaluable during testing and development, enabling you to simulate server responses without relying on a live backend. One approach is to load mock responses from the Android assets folder. Here's how you can incorporate mock responses using Retrofit:

  • Create a folder named mock_responses in the assets directory of your Android project.

  • Save the mock response files as JSON files in the mock_responses folder. For example, get_user_success.json might contain the mock response for the GET request to retrieve a user's data.

  • Implement a custom Interceptor that intercepts the HTTP requests and returns the corresponding mock response if it matches the request's URL.

class MockInterceptor(private val context: Context) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request()
        val response = getMockResponse(request.url.toString())
        return response ?: chain.proceed(request)
    }

    private fun getMockResponse(requestUrl: String): Response? {
        val path = requestUrl.substring(BASE_URL.length) // Adjust as per your base URL structure
        val fileName = "mock_responses/$path.json"
        val inputStream = context.assets.open(fileName)
        val source = inputStream.source().buffer()
        val mockResponse = Response.Builder()
            .code(200)
            .protocol(Protocol.HTTP_1_1)
            .message("Mock response")
            .body(ResponseBody.create("application/json".toMediaTypeOrNull(), source.readString(Charsets.UTF_8)))
            .build()
        inputStream.close()
        return mockResponse
    }
}
  • Add the MockInterceptor to your Retrofit instance when building it:
val interceptor = MockInterceptor(context) // Replace `context` with your application context
val httpClient = OkHttpClient.Builder()
    .addInterceptor(interceptor)
    .build()

val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .client(httpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

// Use the Retrofit instance as usual

Note:

  • Add MockInterceptor only when your application is in DEBUG Mode.

  • To simulate different kinds of methods calling for a single `ENDPOINT` (ex: update_user_info) use chain.request.method() (ex: if (chain.request.method() == "get") or if (chain.request.method() == "post") or others (PUT, PATCH)


That's it for today. Happy coding...

0
Subscribe to my newsletter

Read articles from Romman Sabbir directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Romman Sabbir
Romman Sabbir

Senior Android Engineer from Bangladesh. Love to contribute in Open-Source. Indie Music Producer.