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

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>
}

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)
}
}
}
}
}

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
- Never hardcode your base URL in production. Use build variants or BuildConfig fields.
- Use interceptors for authentication tokens instead of sending them manually on every call.
- Cache responses with OkHttp’s Cache to reduce bandwidth and improve UX.
- Use dependency injection (Hilt or Koin) instead of a global singleton for testability.
- Wrap responses in a Result sealed class at the repository layer to keep ViewModels clean.
- 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.
