Building a REST API Client in Kotlin Using Retrofit and Coroutines

In modern Android development, interacting with RESTful APIs is a common requirement. Kotlin, with its modern syntax and powerful features, makes building REST API clients efficient and straightforward. Retrofit, a type-safe HTTP client for Android and Java, coupled with Kotlin Coroutines, simplifies asynchronous network requests and makes your code more readable and maintainable. This guide demonstrates how to build a REST API client in Kotlin using Retrofit and Coroutines, complete with detailed code examples.

What is Retrofit?

Retrofit is a type-safe HTTP client library for Android and Java, developed by Square. It simplifies the process of making network requests by converting API endpoints into Kotlin interfaces. Retrofit handles the complexities of HTTP communication, such as serialization, deserialization, and request execution, allowing developers to focus on defining API interfaces.

Why Use Kotlin Coroutines with Retrofit?

Kotlin Coroutines provide a way to write asynchronous code in a sequential, non-blocking manner. When combined with Retrofit, they simplify handling network responses asynchronously, improving app performance and user experience by preventing the main thread from blocking.

Prerequisites

Before you start, ensure you have the following:

  • Android Studio installed.
  • Basic understanding of Kotlin and Android development.
  • Familiarity with Gradle.

Step-by-Step Guide

Follow these steps to build a REST API client using Kotlin, Retrofit, and Coroutines.

Step 1: Add Dependencies

Add the required dependencies to your build.gradle.kts (Kotlin DSL) or build.gradle (Groovy DSL) file.

Kotlin DSL (build.gradle.kts):
plugins {
    id("org.jetbrains.kotlin.android")
    id("com.android.application")
}

android {
    namespace = "com.example.retrofitexample"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.retrofitexample"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        viewBinding = true
    }
}

dependencies {

    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // Kotlin Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    //Retrofit Coroutines Support
    implementation("com.squareup.retrofit2:converter-scalars:2.9.0")
    implementation("com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2")

    // Lifecycle Components (optional, but recommended)
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
}
Groovy DSL (build.gradle):
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
}

android {
    namespace 'com.example.retrofitexample'
    compileSdk 34

    defaultConfig {
        applicationId "com.example.retrofitexample"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        viewBinding true
    }
}

dependencies {

    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'com.google.android.material:material:1.11.0'
    implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

    // Retrofit
    implementation 'com.squareup.retrofit2:retrofit:2.9.0'
    implementation 'com.squareup.retrofit2:converter-gson:2.9.0'

    // Kotlin Coroutines
    implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'

     //Retrofit Coroutines Support
    implementation 'com.squareup.retrofit2:converter-scalars:2.9.0'
    implementation 'com.jakewharton.retrofit:retrofit2-kotlin-coroutines-adapter:0.9.2'


    // Lifecycle Components (optional, but recommended)
    implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
}

Make sure to sync your Gradle project after adding these dependencies.

Step 2: Define the API Interface

Create a Kotlin interface that defines the API endpoints. Use Retrofit annotations to specify the HTTP method (e.g., GET, POST), the relative URL, and the request/response types.

For example, let’s define an interface for fetching a list of posts from a mock API:

import retrofit2.http.GET
import retrofit2.http.Path
import retrofit2.Response
import retrofit2.http.POST
import retrofit2.http.Body

interface ApiService {
    @GET("posts")
    suspend fun getPosts(): Response<List<Post>>

    @GET("posts/{id}")
    suspend fun getPost(@Path("id") id: Int): Response<Post>

    @POST("posts")
    suspend fun createPost(@Body post: Post): Response<Post>
}

Step 3: Create Data Models

Define Kotlin data classes to represent the structure of the JSON data you expect to receive from the API.

data class Post(
    val userId: Int,
    val id: Int,
    val title: String,
    val body: String
)

Step 4: Configure Retrofit

Create a Retrofit instance using the Retrofit.Builder class. Configure the base URL of the API and add a converter factory for JSON serialization/deserialization (Gson is commonly used).

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    val instance: ApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        retrofit.create(ApiService::class.java)
    }
}

Step 5: Use the API Client in Your Activity/Fragment

In your Activity or Fragment, call the API using the Retrofit instance and handle the response using Kotlin Coroutines.

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import android.util.Log

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main) // Replace with your layout

        lifecycleScope.launch {
            try {
                val response = RetrofitClient.instance.getPosts()
                if (response.isSuccessful) {
                    val posts = response.body()
                    Log.d("MainActivity", "Posts: $posts")
                    // Handle the list of posts (e.g., display in a RecyclerView)
                } else {
                    Log.e("MainActivity", "Error: ${response.code()}")
                    // Handle the error
                }
            } catch (e: Exception) {
                Log.e("MainActivity", "Exception: ${e.localizedMessage}")
                // Handle the exception (e.g., network error)
            }
        }
    }
}

Step 6: Adding Internet Permission

Ensure your AndroidManifest.xml file includes the necessary internet permission:

<uses-permission android:name="android.permission.INTERNET" />

Complete Example

Here’s a complete example including all the steps.

build.gradle.kts:

plugins {
    id("org.jetbrains.kotlin.android")
    id("com.android.application")
}

android {
    namespace = "com.example.retrofitexample"
    compileSdk = 34

    defaultConfig {
        applicationId = "com.example.retrofitexample"
        minSdk = 24
        targetSdk = 34
        versionCode = 1
        versionName = "1.0"

        testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
    }

    buildTypes {
        release {
            isMinifyEnabled = false
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
    compileOptions {
        sourceCompatibility = JavaVersion.VERSION_1_8
        targetCompatibility = JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = "1.8"
    }
    buildFeatures {
        viewBinding = true
    }
}

dependencies {

    implementation("androidx.core:core-ktx:1.12.0")
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    testImplementation("junit:junit:4.13.2")
    androidTestImplementation("androidx.test.ext:junit:1.1.5")
    androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")

    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // Kotlin Coroutines
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // Lifecycle Components (optional, but recommended)
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.6.2")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.6.2")
}

ApiService.kt:

import retrofit2.http.GET
import retrofit2.Response

interface ApiService {
    @GET("posts")
    suspend fun getPosts(): Response<List<Post>>
}

Post.kt:

data class Post(
    val userId: Int,
    val id: Int,
    val title: String,
    val body: String
)

RetrofitClient.kt:

import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {
    private const val BASE_URL = "https://jsonplaceholder.typicode.com/"

    val instance: ApiService by lazy {
        val retrofit = Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()

        retrofit.create(ApiService::class.java)
    }
}

MainActivity.kt:

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import android.util.Log

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        //setContentView(R.layout.activity_main) // Replace with your layout

        lifecycleScope.launch {
            try {
                val response = RetrofitClient.instance.getPosts()
                if (response.isSuccessful) {
                    val posts = response.body()
                    Log.d("MainActivity", "Posts: $posts")
                    // Handle the list of posts (e.g., display in a RecyclerView)
                } else {
                    Log.e("MainActivity", "Error: ${response.code()}")
                    // Handle the error
                }
            } catch (e: Exception) {
                Log.e("MainActivity", "Exception: ${e.localizedMessage}")
                // Handle the exception (e.g., network error)
            }
        }
    }
}

AndroidManifest.xml:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.retrofitexample">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.RetrofitExample">
        <activity
            android:name=".MainActivity"
            android:exported="true">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

Error Handling

It’s important to handle potential errors when making network requests. Use try-catch blocks to catch exceptions, and check the HTTP response status to handle server-side errors.

Conclusion

Building a REST API client in Kotlin using Retrofit and Coroutines simplifies asynchronous network operations. By following this guide, you can efficiently fetch data from RESTful APIs and integrate it into your Android applications, providing a seamless and responsive user experience. Leveraging Retrofit’s type-safe interface and Kotlin Coroutines’ asynchronous capabilities enhances the reliability and maintainability of your code.