On this page
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
Setup
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>")
}<uses-permission android:name="android.permission.INTERNET" />Models
@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
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
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
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(withcode()) when the return type isn'tResponse<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
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.
Walzi4K wallpapers for lock and home screenGet it โธ
NotiVaultNotification history: recover dismissed notificationsGet it โธ
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.