๐Ÿ”Œ APIs, Databases, and Backend Integration

Retrofit in Android: Complete Guide

Retrofit turns an HTTP API into a Kotlin interface. Combined with coroutines and kotlinx.serialization, a network call becomes a single suspend function. This guide covers setup, every common request type, configuration, error handling and testing.

โœ๏ธ Yunok Tech ๐Ÿ“… Updated โฑ 3 min read ๐ŸŽฏ Beginner
On this page
  1. How Retrofit works
  2. Setup
  3. Models
  4. Defining the API
  5. Building the client
  6. Calling from a repository
  7. Errors
  8. Testing with MockWebServer
  9. Common real-world needs
  10. Common mistakes
  11. Frequently asked questions
  12. Conclusion

Most Android apps talk to a REST API. Writing raw HTTP code (building URLs, encoding JSON, parsing responses, handling threads) is repetitive and error-prone. Retrofit, from Square, generates all of that from an annotated interface, and it's the most widely used HTTP client in the Android ecosystem.

How Retrofit works

Retrofit request pipeline Your code calls a suspend function on an annotated interface. Retrofit builds an HTTP request, the converter serialises the body, OkHttp sends it through interceptors to the server, and the response JSON is deserialised back into a Kotlin data class. api.getUser(id)suspend call Retrofitbuilds request OkHttpinterceptors Server UserDtoparsed Converter (kotlinx.serialization) handles JSON โ†” data classes OkHttp does the network work off the main thread
You write the interface; Retrofit, a converter and OkHttp do the rest.

Setup

app/build.gradle.ktsGradle KTS
plugins { id("org.jetbrains.kotlin.plugin.serialization") }
dependencies {
    implementation("com.squareup.retrofit2:retrofit:<version>")
    implementation("com.squareup.retrofit2:converter-kotlinx-serialization:<version>")
    implementation("com.squareup.okhttp3:okhttp:<version>")
    implementation("com.squareup.okhttp3:logging-interceptor:<version>")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:<version>")
}
AndroidManifest.xmlXML
<uses-permission android:name="android.permission.INTERNET" />

Models

Dtos.ktKotlin
@Serializable
data class UserDto(
    val id: String,
    val name: String,
    @SerialName("avatar_url") val avatarUrl: String? = null,   // map snake_case, tolerate missing
)

@Serializable
data class CreatePostRequest(val title: String, val body: String, val tags: List<String> = emptyList())

@Serializable
data class PostDto(val id: Long, val title: String, val body: String)

Defining the API

BlogApi.ktKotlin
interface BlogApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") id: String): UserDto

    @GET("posts")
    suspend fun getPosts(
        @Query("page") page: Int,
        @Query("limit") limit: Int = 20,
        @Query("tag") tag: String? = null,          // null query params are omitted
    ): List<PostDto>

    @POST("posts")
    suspend fun createPost(@Body request: CreatePostRequest): PostDto

    @DELETE("posts/{id}")
    suspend fun deletePost(@Path("id") id: Long): Response<Unit>   // Response<T> when you need status/headers

    @Multipart
    @POST("uploads")
    suspend fun upload(@Part file: MultipartBody.Part): UploadResult
}

Building the client

NetworkModule.ktKotlin
object Network {
    private val json = Json {
        ignoreUnknownKeys = true      // new server fields won't crash old app versions
        explicitNulls = false
    }

    private val okHttp = OkHttpClient.Builder()
        .connectTimeout(15, TimeUnit.SECONDS)
        .readTimeout(30, TimeUnit.SECONDS)
        .addInterceptor(HttpLoggingInterceptor().apply {
            level = if (BuildConfig.DEBUG) HttpLoggingInterceptor.Level.BODY else HttpLoggingInterceptor.Level.NONE
        })
        .build()

    val blogApi: BlogApi = Retrofit.Builder()
        .baseUrl("https://api.example.com/v1/")     // must end with "/"
        .client(okHttp)
        .addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
        .build()
        .create(BlogApi::class.java)
}

In a real app, provide these through Hilt as singletons. See Hilt. Never log bodies or auth headers in release builds.

Calling from a repository

PostRepository.ktKotlin
class PostRepository(private val api: BlogApi) {
    suspend fun latestPosts(page: Int): List<Post> = api.getPosts(page).map { it.toDomain() }
}

Retrofit's suspend functions are main-safe: OkHttp performs the I/O on its own threads, so you can call them from viewModelScope directly.

Errors

  • Non-2xx responses throw HttpException (with code()) when the return type isn't Response<T>.
  • Network failures (no connection, timeouts) throw IOException.
  • Malformed JSON throws a SerializationException.

Map these to user-friendly states in one place. See Handling API Errors and Network Failures.

Testing with MockWebServer

BlogApiTest.ktKotlin
class BlogApiTest {
    private val server = MockWebServer()
    private lateinit var api: BlogApi

    @Before fun setUp() {
        server.start()
        api = Retrofit.Builder()
            .baseUrl(server.url("/"))
            .addConverterFactory(Json { ignoreUnknownKeys = true }.asConverterFactory("application/json".toMediaType()))
            .build().create(BlogApi::class.java)
    }
    @After fun tearDown() = server.shutdown()

    @Test fun `parses user and ignores unknown fields`() = runTest {
        server.enqueue(MockResponse().setBody("""{"id":"7","name":"Asha","extra":true}"""))
        val user = api.getUser("7")
        assertEquals("Asha", user.name)
        assertEquals("/users/7", server.takeRequest().path)
    }
}

Common real-world needs

Auth headers

An OkHttp interceptor adds the token, and an authenticator refreshes it on 401. See OkHttp Interceptors.

Pagination

Query params for page and cursor, wired into Paging 3.

File uploads

@Multipart with a request body streamed from a ContentResolver URI.

Common mistakes

  • Creating a new Retrofit or OkHttp instance per request, which wastes connection pools and caches.
  • Base URL without a trailing slash, or leading slashes in endpoint paths, which produce surprising URL resolution.
  • Strict JSON parsing without ignoreUnknownKeys, so new server fields crash old app versions.
  • Using DTOs directly in UI state. Map them to domain models.
  • Logging sensitive data in production.

Frequently asked questions

Gson, Moshi or kotlinx.serialization?

For Kotlin-first apps, kotlinx.serialization respects default values and nullability and works with Kotlin Multiplatform. Moshi is also solid. Gson ignores Kotlin nullability and defaults, so it's less suited to new Kotlin code.

Do I need Dispatchers.IO for Retrofit calls?

No. Suspend Retrofit functions are already main-safe.

How do I cancel a request?

Cancel the coroutine. Retrofit cancels the underlying OkHttp call automatically.

Conclusion

Retrofit + OkHttp + kotlinx.serialization + coroutines give you type-safe, concise, main-safe networking. Share one configured client, tolerate unknown JSON fields, map DTOs to domain models, handle errors centrally, and test with MockWebServer.

Next step: build a complete flow around it in REST API Integration with Kotlin and Coroutines.

Official references

๐Ÿ“ฑ Apps built by Yunok Tech

Real Android apps on Google Play, free to download.

See all our apps โ†’

Ready to build real Android applications?

Networking, persistence and offline sync are where real apps get complicated. Practise building a data layer that handles slow networks, failures and caching, not just the happy path.

Move beyond theory and strengthen your Android development skills with Yunoktech's Android course. Learn core concepts, practical implementation, modern Android development techniques, and the reasoning behind building maintainable applications.

Explore the Android course and take your next step as an Android developer.

Share: LinkedIn X / Twitter WhatsApp