android Retrofit Kotlin Android API REST API Networking Beginner Coroutines

How to Make API Calls in Android with Retrofit and Kotlin — Complete Beginner Guide

GET, POST, PUT, DELETE — all covered with real examples

How to Make API Calls in Android with Retrofit and Kotlin — Complete Beginner Guide

Almost every Android app talks to the internet — fetching posts, logging in users, saving data to a server. The library that makes this easy in Android is Retrofit. It is the most widely used networking library in Android development, and once you understand it, building connected apps becomes straightforward.

In this guide you will go from zero to making real API calls — GET, POST, PUT, and DELETE — all in Kotlin with coroutines.

---

What is Retrofit?

Retrofit is a type-safe HTTP client for Android. Instead of writing raw network code, you define your API endpoints as simple Kotlin interfaces and Retrofit handles everything else — making the request, parsing the response, handling errors.

Think of it as a translator between your app and the server.

---

What We Will Build

We will use the free JSONPlaceholder API — a fake REST API for testing. No signup needed. We will:

GET a list of posts from the server

GET a single post by ID

POST a new post to the server

PUT (update) an existing post

DELETE a post

💡 JSONPlaceholder (jsonplaceholder.typicode.com) is a free fake API perfect for learning. All requests work and return realistic data — nothing is actually saved on their server.

---

Step 1 — Add Dependencies

Add these to your build.gradle (app):

groovybuild.gradle
dependencies {
    // Retrofit
    implementation("com.squareup.retrofit2:retrofit:2.9.0")

    // Gson converter — converts JSON to Kotlin objects automatically
    implementation("com.squareup.retrofit2:converter-gson:2.9.0")

    // Coroutines for async calls
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3")

    // ViewModel and lifecycle
    implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0")
}

Also add internet permission to your AndroidManifest.xml:

xmlAndroidManifest.xml
<uses-permission android:name="android.permission.INTERNET" />
⚠️ Forgetting the INTERNET permission is the number one reason Retrofit calls fail silently. Always add it before testing.

---

Step 2 — Create Your Data Model

A data model is a Kotlin class that represents the JSON response. Gson will automatically map JSON fields to your class properties.

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

This matches the JSON structure from JSONPlaceholder:

json
{
  "id": 1,
  "userId": 1,
  "title": "Sample Post Title",
  "body": "This is the post content."
}
💡 Your Kotlin property names must match the JSON field names exactly, or use @SerializedName("json_field_name") annotation to map them manually.

---

Step 3 — Create the API Interface

This is where you define all your endpoints. Retrofit reads these annotations and builds the actual HTTP requests for you.

kotlinPostApiService.kt
import retrofit2.http.*

interface PostApiService {

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

    // GET single post by ID
    @GET("posts/{id}")
    suspend fun getPostById(@Path("id") id: Int): Post

    // POST — create a new post
    @POST("posts")
    suspend fun createPost(@Body post: Post): Post

    // PUT — update an existing post
    @PUT("posts/{id}")
    suspend fun updatePost(
        @Path("id") id: Int,
        @Body post: Post
    ): Post

    // DELETE a post
    @DELETE("posts/{id}")
    suspend fun deletePost(@Path("id") id: Int)
}

Breaking down the annotations:

@GET("posts") — makes a GET request to /posts

@POST("posts") — makes a POST request to /posts

@PUT("posts/{id}") — makes a PUT request to /posts/1 (for example)

@DELETE("posts/{id}") — makes a DELETE request

@Path("id") — replaces {id} in the URL with the actual value

@Body — sends the object as JSON in the request body

suspend — makes the function work with coroutines

---

Step 4 — Build the Retrofit Instance

Create a singleton Retrofit client. This is where you set the base URL and tell Retrofit to use Gson for JSON parsing.

kotlinRetrofitClient.kt
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory

object RetrofitClient {

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

    val apiService: PostApiService by lazy {
        Retrofit.Builder()
            .baseUrl(BASE_URL)
            .addConverterFactory(GsonConverterFactory.create())
            .build()
            .create(PostApiService::class.java)
    }
}
💡 Using lazy means the Retrofit instance is only created the first time you access it — not when the app starts. This saves memory and startup time.

---

Step 5 — Create a Repository

A repository is a clean layer between your API and your ViewModel. It handles the actual API calls and wraps results safely.

kotlinPostRepository.kt
class PostRepository {

    private val api = RetrofitClient.apiService

    suspend fun getAllPosts(): Result<List<Post>> {
        return try {
            val posts = api.getPosts()
            Result.success(posts)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun getPost(id: Int): Result<Post> {
        return try {
            Result.success(api.getPostById(id))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun createPost(post: Post): Result<Post> {
        return try {
            Result.success(api.createPost(post))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun updatePost(id: Int, post: Post): Result<Post> {
        return try {
            Result.success(api.updatePost(id, post))
        } catch (e: Exception) {
            Result.failure(e)
        }
    }

    suspend fun deletePost(id: Int): Result<Unit> {
        return try {
            api.deletePost(id)
            Result.success(Unit)
        } catch (e: Exception) {
            Result.failure(e)
        }
    }
}

---

Step 6 — ViewModel

The ViewModel calls the repository and exposes the results to your UI using StateFlow.

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

class PostViewModel : ViewModel() {

    private val repository = PostRepository()

    private val _posts = MutableStateFlow<List<Post>>(emptyList())
    val posts: StateFlow<List<Post>> = _posts

    private val _error = MutableStateFlow<String?>(null)
    val error: StateFlow<String?> = _error

    init {
        fetchPosts()
    }

    fun fetchPosts() {
        viewModelScope.launch {
            repository.getAllPosts()
                .onSuccess { _posts.value = it }
                .onFailure { _error.value = it.message }
        }
    }

    fun createNewPost(title: String, body: String) {
        viewModelScope.launch {
            val newPost = Post(id = 0, userId = 1, title = title, body = body)
            repository.createPost(newPost)
                .onSuccess { fetchPosts() }
                .onFailure { _error.value = it.message }
        }
    }

    fun deletePost(id: Int) {
        viewModelScope.launch {
            repository.deletePost(id)
                .onSuccess { fetchPosts() }
                .onFailure { _error.value = it.message }
        }
    }
}

---

Step 7 — Collect in Your Activity

kotlinMainActivity.kt
class MainActivity : AppCompatActivity() {

    private val viewModel: PostViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {

                // observe posts
                launch {
                    viewModel.posts.collect { posts ->
                        // update your RecyclerView adapter
                        adapter.submitList(posts)
                    }
                }

                // observe errors
                launch {
                    viewModel.error.collect { error ->
                        error?.let {
                            Toast.makeText(this@MainActivity, it, Toast.LENGTH_SHORT).show()
                        }
                    }
                }
            }
        }
    }
}

---

Common Mistakes Beginners Make

1. Missing INTERNET permission

🚨 If you forget in AndroidManifest.xml, all your network calls will fail with no useful error message.

2. Making network calls on the main thread

⚠️ Never call Retrofit functions directly on the main thread. Always use suspend functions inside viewModelScope.launch or a coroutine. Retrofit will throw a NetworkOnMainThreadException otherwise.

3. Wrong BASE_URL format

The base URL must end with a forward slash. This is correct:

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

This will crash:

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

4. Not handling errors

Network calls can fail for many reasons — no internet, server down, timeout. Always wrap calls in try-catch or use Result as shown above.

---

What's Next?

Add OkHttp logging interceptor to see full request and response logs in Logcat

Use Hilt or Koin for dependency injection instead of manual instantiation

Add loading states to your ViewModel so the UI shows a spinner while fetching

Explore Retrofit with authentication headers for protected APIs

---

Summary

Retrofit turns your API endpoints into simple Kotlin interface functions

Gson converter automatically maps JSON to your data classes

Use suspend functions with coroutines for all network calls

Always wrap network calls in try-catch and handle failures gracefully

Never forget the INTERNET permission in AndroidManifest.xml

Retrofit is one of those libraries that once you learn it, you use it in every Android project. The pattern is always the same — define the interface, build the client, call from a coroutine. Master this and you can connect any Android app to any REST API.

Asif Rahman
Asif Rahman

Indie Product Engineer focused on toolcraft — building free tools that just work.

← Back to Blog Try Free Tools ⚡