Skip to content
All posts
July 30, 20264 min read

Ktor Client in Android: REST API Calls the Modern Way

Ktor is a clean, coroutine-native HTTP client that shines especially in Kotlin Multiplatform projects. Here's how I set it up for Android — serialization, auth, retries, and error handling — and when I'd still pick Retrofit.

KtorNetworkingAndroidKotlinREST
Share:

Retrofit has been the default Android HTTP client for years, and it's excellent. But if you're working in Kotlin Multiplatform — or just want a client built coroutine-first from the ground up — Ktor is the modern alternative. It runs on every Kotlin target, it's configured in plain Kotlin rather than annotations, and once it's set up it's a pleasure to use. Here's the practical setup I use on Android.

Configure the Client Once

A Ktor client is built once with the plugins you need installed on it. Content negotiation handles serialization, and I pair it with kotlinx.serialization rather than Gson — it's the multiplatform-friendly choice and it's compile-time safe.

kotlin
val client = HttpClient(CIO) {
    install(ContentNegotiation) {
        json(Json { ignoreUnknownKeys = true })
    }
    install(HttpTimeout) {
        requestTimeoutMillis = 15_000
    }
}

code
ignoreUnknownKeys
is the setting that saves you when the API adds a field you didn't model — without it, an unexpected key throws. Configuring the client once and reusing it matters: creating a new client per request leaks resources.

Making Requests Is Just Suspend Functions

There's no interface to define — a request is a suspend function call that returns your typed model directly.

kotlin
suspend fun getUser(id: String): User =
    client.get("https://api.example.com/users/$id").body()

That directness is the appeal. The request reads like what it does, the

code
body()
deserializes into your data class via the content negotiation you configured, and it all suspends cleanly inside a coroutine.

Authentication via Plugins

Auth is handled by installing the

code
Auth
plugin or a custom header provider, so you set it up once rather than attaching a token to every call. For bearer tokens, the plugin can also handle refreshing them when they expire, which centralizes the token logic instead of scattering it. Keeping auth in the client configuration means individual request code stays clean and there's a single place to reason about how credentials are attached.

Retries and Error Handling

Networks fail, and a robust client plans for it. Ktor's

code
HttpRequestRetry
plugin handles transient failures with backoff so a momentary blip doesn't surface as an error to the user.

kotlin
install(HttpRequestRetry) {
    retryOnServerErrors(maxRetries = 3)
    exponentialDelay()
}

For error handling, I wrap calls so HTTP errors and exceptions map to a sealed result type the UI can render — never letting a raw exception escape to the screen. This fits the Clean Architecture pattern of surfacing failures as explicit UI states rather than crashes. The combination of retries for transient issues and explicit mapping for real failures is what makes the network layer feel reliable.

Ktor or Retrofit?

I won't pretend Ktor is strictly better — the right choice depends on the project. For a Kotlin Multiplatform app, Ktor is the clear pick because Retrofit is Android-only; sharing the networking layer across platforms is the whole reason to use it. For an Android-only app, Retrofit remains a perfectly good, mature, well-documented choice, and there's no reason to migrate working Retrofit code just for fashion. My rule is simple: Ktor for multiplatform, Retrofit for Android-only, and consistency within a project over chasing the newer tool. Both are coroutine-friendly and both do the job well; pick based on where the code needs to run.

What I appreciate most about Ktor after living with it is how little magic there is. There's no annotation processor generating code you can't see, no interface whose behavior is defined elsewhere — the client is an object you configure in plain Kotlin, and a request is a function call. That transparency makes it easy to reason about and easy to debug, because the behavior is right there in the configuration rather than hidden behind generated glue. For a multiplatform project that transparency is paired with the genuine payoff of running everywhere, which is the combination that makes Ktor worth learning. As always, the goal isn't to use the trendiest client; it's to use the one whose trade-offs fit the project, and to stay consistent once you've chosen so the networking layer is one fewer thing anyone has to think about.

Key Takeaways

  • Build one Ktor client with the plugins you need and reuse it; per-request clients leak resources.
  • Use kotlinx.serialization with content negotiation, and set
    code
    ignoreUnknownKeys
    so new API fields don't crash you.
  • Requests are typed suspend functions returning your model directly — no interface boilerplate.
  • Centralize auth in the Auth plugin, including token refresh, so request code stays clean.
  • Add the retry plugin with backoff for transient failures and map real errors to a sealed result the UI can render.
  • Choose Ktor for multiplatform and Retrofit for Android-only; favor consistency over chasing the newer client.
Share:
S

Sudarshan Chaudhari

AI Systems Builder / Product Engineer

Bangkok, Thailand

Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.

Stay updated

Get new posts on Android, Kotlin, and solo dev straight to your inbox.

Newsletter preferences

Related Apps

MyFamilyTracker

Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.

Building something? Available for Android dev and QA consulting.

Work with me

Comments — powered by Giscus

Apps tagged with this