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
---
Step 1 — Add Dependencies
Add these to your build.gradle (app):
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:
<uses-permission android:name="android.permission.INTERNET" />---
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.
data class Post(
val id: Int,
val userId: Int,
val title: String,
val body: String
)This matches the JSON structure from JSONPlaceholder:
{
"id": 1,
"userId": 1,
"title": "Sample Post Title",
"body": "This is the post content."
}---
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.
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.
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)
}
}---
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.
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.
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
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
2. Making network calls on the main thread
3. Wrong BASE_URL format
The base URL must end with a forward slash. This is correct:
private const val BASE_URL = "https://jsonplaceholder.typicode.com/"This will crash:
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.