How to Integrate a REST API in an Android App with Kotlin: Step-by-Step Tutorial

How to Integrate a REST API in an Android App with Kotlin: Step-by-Step Tutorial

by | Sep 30, 2026 | Uncategorized | 0 comments

Connecting your Android app to a backend is one of the most common tasks you will face as a mobile developer. In this practical, code-heavy tutorial, we will walk through how to integrate a REST API in an Android app with Kotlin using Retrofit, the most battle-tested HTTP client in the Android ecosystem. No fluff, just working snippets you can copy, paste, and adapt to your own project.

Why Retrofit for REST API Integration in Android?

Before diving into the code, let’s quickly justify our tech choices. When you need to integrate a REST API in Android with Kotlin, you have a few options: HttpURLConnection, OkHttp raw, Ktor Client, or Retrofit. Retrofit remains the go-to for most production apps because:

  • It generates network code from a simple Kotlin interface
  • It integrates natively with Kotlin coroutines and Flow
  • It handles serialization automatically with Moshi or Kotlinx Serialization
  • It is maintained by Square and widely adopted in the industry
android kotlin code

Step 1: Add Dependencies to Your Project

Open your build.gradle.kts (app module) and add the following dependencies. These versions are current as of 2026:

dependencies {
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.11.0")
    implementation("com.squareup.retrofit2:converter-moshi:2.11.0")

    // OkHttp logging interceptor
    implementation("com.squareup.okhttp3:logging-interceptor:4.12.0")

    // Moshi for JSON parsing
    implementation("com.squareup.moshi:moshi-kotlin:1.15.1")

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

    // ViewModel + Lifecycle
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.8.4")
    implementation("androidx.lifecycle:lifecycle-runtime-ktx:2.8.4")
}

Step 2: Add Internet Permission

In your AndroidManifest.xml, add the internet permission just above the <application> tag:

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

Step 3: Define Your Data Models

Let’s assume we are consuming the JSONPlaceholder API (a great free API for testing). A single post looks like this:

{
  "userId": 1,
  "id": 1,
  "title": "sunt aut facere",
  "body": "quia et suscipit\nsuscipit"
}

Create a Kotlin data class that mirrors this structure:

import com.squareup.moshi.JsonClass

@JsonClass(generateAdapter = true)
data class Post(
    val userId: Int,
    val id: Int,
    val title: String,
    val body: String
)

Step 4: Create the Retrofit API Interface

This is where the magic happens. Define a Kotlin interface describing your endpoints. Retrofit will implement it for you at runtime. There is more on it in Retrieve data from web sources and APIs.

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

interface PostsApi {

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

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

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

Step 5: Build the Retrofit Client

Create a singleton object to expose your API instance across the app:

import com.squareup.moshi.Moshi
import com.squareup.moshi.kotlin.reflect.KotlinJsonAdapterFactory
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.moshi.MoshiConverterFactory
import java.util.concurrent.TimeUnit

object ApiClient {

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

    private val logging = HttpLoggingInterceptor().apply {
        level = HttpLoggingInterceptor.Level.BODY
    }

    private val okHttpClient = OkHttpClient.Builder()
        .addInterceptor(logging)
        .connectTimeout(30, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .build()

    private val moshi = Moshi.Builder()
        .add(KotlinJsonAdapterFactory())
        .build()

    val postsApi: PostsApi by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .client(okHttpClient)
            .addConverterFactory(MoshiConverterFactory.create(moshi))
            .build()
            .create(PostsApi::class.java)
    }
}

Step 6: Perform a GET Request from a ViewModel

Never call the network from the UI thread. Use a ViewModel with coroutines instead:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.launch

sealed interface PostsUiState {
    data object Loading : PostsUiState
    data class Success(val posts: List<Post>) : PostsUiState
    data class Error(val message: String) : PostsUiState
}

class PostsViewModel : ViewModel() {

    private val _uiState = MutableStateFlow<PostsUiState>(PostsUiState.Loading)
    val uiState: StateFlow<PostsUiState> = _uiState.asStateFlow()

    fun loadPosts() {
        viewModelScope.launch {
            _uiState.value = PostsUiState.Loading
            try {
                val response = ApiClient.postsApi.getPosts()
                if (response.isSuccessful) {
                    val body = response.body().orEmpty()
                    _uiState.value = PostsUiState.Success(body)
                } else {
                    _uiState.value = PostsUiState.Error("HTTP ${response.code()}")
                }
            } catch (e: Exception) {
                _uiState.value = PostsUiState.Error(e.message ?: "Unknown error")
            }
        }
    }
}

Step 7: Send a POST Request

Sending data is just as straightforward. Here is how you POST a new resource:

fun createNewPost(title: String, body: String) {
    viewModelScope.launch {
        try {
            val newPost = Post(userId = 1, id = 0, title = title, body = body)
            val response = ApiClient.postsApi.createPost(newPost)
            if (response.isSuccessful) {
                val created = response.body()
                // Handle success
            } else {
                // Handle server error
            }
        } catch (e: Exception) {
            // Handle network error
        }
    }
}

Step 8: Display Results in Jetpack Compose

Here’s a minimal Compose screen that observes the ViewModel state:

@Composable
fun PostsScreen(viewModel: PostsViewModel = viewModel()) {
    val state by viewModel.uiState.collectAsStateWithLifecycle()

    LaunchedEffect(Unit) { viewModel.loadPosts() }

    when (val s = state) {
        is PostsUiState.Loading -> CircularProgressIndicator()
        is PostsUiState.Error -> Text("Error: ${s.message}")
        is PostsUiState.Success -> LazyColumn {
            items(s.posts) { post ->
                Column(Modifier.padding(16.dp)) {
                    Text(post.title, style = MaterialTheme.typography.titleMedium)
                    Text(post.body)
                }
            }
        }
    }
}
android kotlin code

Handling Errors Properly

Robust error handling is what separates a demo from a production app. Here is a summary of the errors you will encounter and how to react to them:

Error Type Cause Recommended Action
IOException No internet, DNS failure Show retry button
HTTP 4xx Client error (bad request, auth) Show contextual message, refresh token if 401
HTTP 5xx Server error Retry with exponential backoff
JsonDataException Malformed JSON or schema mismatch Log to Crashlytics, show generic error

Best Practices When You Integrate a REST API in Android with Kotlin

  1. Never hardcode your base URL in production. Use build variants or BuildConfig fields.
  2. Use interceptors for authentication tokens instead of sending them manually on every call.
  3. Cache responses with OkHttp’s Cache to reduce bandwidth and improve UX.
  4. Use dependency injection (Hilt or Koin) instead of a global singleton for testability.
  5. Wrap responses in a Result sealed class at the repository layer to keep ViewModels clean.
  6. Enable ProGuard/R8 rules for Moshi and Retrofit to avoid release-build crashes.

Bonus: Adding an Auth Interceptor

Most real APIs require a bearer token. Here is a clean way to inject it:

class AuthInterceptor(private val tokenProvider: () -> String?) : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val request = chain.request().newBuilder().apply {
            tokenProvider()?.let { addHeader("Authorization", "Bearer $it") }
        }.build()
        return chain.proceed(request)
    }
}

Add it to your OkHttpClient builder alongside the logging interceptor.

Conclusion

You now have a full, production-ready pattern to integrate a REST API in an Android app with Kotlin. Retrofit combined with Kotlin coroutines, Moshi, and a well-structured ViewModel gives you a stack that is both concise and robust. Copy the snippets above into your project, swap in your own base URL and data models, and you’ll have working network calls in under an hour.

FAQ

Should I use Retrofit or Ktor Client in 2026?

Retrofit is still the safest choice for Android-only apps thanks to its mature ecosystem. Ktor Client shines in Kotlin Multiplatform projects where you share networking code across iOS and Android. There’s a good explainer over at ktor.io.

Can I use Kotlinx Serialization instead of Moshi?

Yes. Simply replace MoshiConverterFactory with the Kotlinx Serialization converter and annotate your data classes with @Serializable. Both work great with Retrofit.

How do I test my Retrofit API calls?

Use OkHttp’s MockWebServer library. It lets you enqueue fake responses and verify the exact requests your app sends, without hitting a real backend.

Do I still need LiveData or should I use StateFlow?

For new projects in 2026, prefer StateFlow combined with collectAsStateWithLifecycle() in Compose. It integrates better with coroutines and is fully Kotlin-native.

How do I handle pagination when consuming a REST API?

Use the Paging 3 library from Jetpack. It integrates smoothly with Retrofit suspend functions and handles loading states, retries, and prefetching automatically. Originally covered on https://developer.android.com.