Skip to content
All posts
September 15, 20267 min read

Retrofit vs Ktor: Which Network Client Should You Ship With?

A solo Android developer compares Retrofit and Ktor across real-world criteria: setup complexity, coroutine support, serialization, and maintenance overhead, with working code examples you can copy today.

AndroidKotlinBest Practices
Share:

You already know both libraries work. The question is which one wastes less of your time.

I have shipped 22 apps using Retrofit, and I switched two of them to Ktor last year. Both are solid. Neither is obviously better. The right choice depends on your project's constraints, your team's familiarity, and how much boilerplate you are willing to tolerate.

The Problem You Are Actually Solving

Every Android app needs HTTP calls. You fetch user profiles, push events to analytics, upload images, and poll for updates. The network client sits between your domain layer and the internet. If it is painful to use, your repository classes bloat, your tests multiply, and your onboarding time for new developers stretches.

Retrofit has been the default for years. Ktor entered the Kotlin ecosystem as a native alternative built by JetBrains. Both support coroutines, both work with Moshi or Gson, and both compile on JVM and Android. But their philosophies differ.

Retrofit treats HTTP as a type-safe REST client. You declare an interface, annotate endpoints, and let the library generate the implementation. Ktor treats HTTP as a flexible engine you configure programmatically. You build requests with builders and DSLs.

I will walk through setup, code style, serialization, error handling, and long-term maintenance so you can decide with confidence.

Setup and Dependencies

Retrofit requires the core library plus a converter for your serialization format. You also add an OkHttp dependency because Retrofit delegates to OkHttp under the hood.

kotlin
// build.gradle.kts
dependencies {
    implementation("com.squareup.retrofit2:retrofit:2.9.0")
    implementation("com.squareup.retrofit2:converter-moshi:2.9.0")
    implementation("com.squareup.okhttp3:okhttp:4.12.0")
    implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.2")
}

Ktor pulls in the client core, the Android engine, and a serialization plugin. The dependency graph is smaller.

kotlin
// build.gradle.kts
dependencies {
    implementation("io.ktor:ktor-client-core:2.3.9")
    implementation("io.ktor:ktor-client-android:2.3.9")
    implementation("io.ktor:ktor-client-content-negotiation:2.3.9")
    implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.9")
}

Both setups take under five minutes. Retrofit's extra dependency on OkHttp is not a burden; OkHttp is battle-tested and gives you interceptors, caching, and connection pooling for free. Ktor bundles its own engine, which means fewer transitive dependencies but a smaller plugin ecosystem.

[!TIP] If you already use OkHttp interceptors for logging or token refresh, Retrofit integrates with them directly. Ktor has its own interceptor model called

code
HttpClientPlugin
, which works differently.

Writing Requests

Retrofit uses annotated interfaces. You declare the HTTP method, path, and parameter annotations on each function. The library generates the implementation at runtime.

kotlin
interface UserApi {
    @GET("users/{id}")
    suspend fun getUser(@Path("id") userId: String): User

    @POST("users")
    suspend fun createUser(@Body user: CreateUserRequest): User

    @GET("search")
    suspend fun searchUsers(@Query("q") query: String, @Query("page") page: Int): SearchResult
}

Ktor uses a builder-style DSL. You construct the request inside a

code
httpClient.get
or
code
httpClient.post
block.

kotlin
val client = HttpClient(Android) {
    install(ContentNegotiation) {
        json()
    }
    baseUrl = "https://api.example.com"
}

suspend fun getUser(userId: String): User {
    return client.get("users/$userId")
}

suspend fun createUser(user: CreateUserRequest): User {
    return client.post("users") {
        setBody(user)
    }
}

The Retrofit style is declarative. The Ktor style is imperative. Retrofit keeps your API surface clean and easy to scan. Ktor gives you more control over headers, body writing, and encoding on a per-request basis.

CriterionRetrofitKtor
Declaration styleAnnotated interfaceBuilder DSL
Per-request headers
code
@Header
or
code
@HeaderMap
code
header()
inside the block
File upload
code
@Multipart
with
code
@Part
code
MultiPartData
builder
WebSocket supportNot built-in (use OkHttp)Native client WebSocket
Generated codeRuntime proxyDirect calls

Serialization

Retrofit delegates serialization to a converter. Moshi and Gson are the most common choices. You annotate your data classes and the converter handles JSON mapping.

kotlin
@Serializable
data class User(
    val id: String,
    val name: String,
    val email: String
)

Ktor uses kotlinx-serialization natively. You mark classes with

code
@Serializable
and the plugin handles encoding and decoding. Both approaches produce clean data classes.

[!NOTE] Retrofit with Moshi supports Kotlin serialization annotations if you use the

code
moshi-kotlin
adapter. Ktor's kotlinx-serialization integration is tighter because both projects share JetBrains ecosystem roots.

The real difference surfaces with polymorphic types. kotlinx-serialization handles sealed classes and inheritance with

code
@SerialName
and polymorphic annotations. Moshi requires custom adapters for the same scenarios. If your API returns nested discriminated unions, Ktor's serialization path is smoother.

Error Handling

Retrofit throws

code
HttpException
for non-2xx responses. You catch it and read the error body manually. OkHttp's
code
Response
object gives you status codes and headers, but Retrofit abstracts that away.

kotlin
try {
    val user = userApi.getUser("123")
} catch (e: HttpException) {
    val errorCode = e.code()
    val errorBody = e.response()?.errorBody()?.string()
    // handle accordingly
}

Ktor throws

code
ClientRequestException
and
code
ServerResponseException
for 4xx and 5xx responses. The exception carries the response object, so you can inspect status, headers, and body without extra calls.

kotlin
try {
    val user = client.get("users/123")
} catch (e: ClientRequestException) {
    val errorCode = e.response.status.value
    val errorBody = e.response.bodyAsText()
}

Both require you to map error bodies to your domain model. Retrofit's approach is more explicit because you control the converter. Ktor's approach is more integrated because the response object is available in the catch block.

[!WARNING] Neither library retries failed requests automatically. If you need retry logic with exponential backoff, add OkHttp's

code
Interceptor
for Retrofit or Ktor's
code
HttpSendPlugin
for Ktor. Do not rely on the base client for resilience.

Testing and Mocking

Retrofit works naturally with MockWebServer from OkHttp. You start the server, enqueue responses, and assert on recorded requests. This is the standard approach in Android testing.

kotlin
@get:Rule
val server = MockWebServer()

@Test
fun `getUser returns user`() = runTest {
    server.enqueue(MockResponse().setBody("""{"id":"1","name":"Test"}"""))
    val api = retrofit.create(UserApi::class.java)
    val user = api.getUser("1")
    assertEquals("Test", user.name)
}

Ktor uses a test engine (

code
TestEngine
) that avoids real network calls. You configure the client with
code
engine = TestEngine
and define response handlers inline.

kotlin
val client = HttpClient(Test) {
    engine {
        addHandler { request ->
            when {
                request.url.encodedPath == "/users/1" -> respondText("""{"id":"1","name":"Test"}""")
                else -> error("Unhandled")
            }
        }
    }
    install(ContentNegotiation) { json() }
}

Both approaches are reliable. MockWebServer is more widely used in the Android community, which means more Stack Overflow answers and more team familiarity. Ktor's test engine is lighter but less documented.

Maintenance and Long-Term Cost

I have maintained apps with Retrofit for 4 years and Ktor for 2 years. Retrofit's API surface has been stable. Square rarely breaks changes. The annotations and interface pattern stay consistent across versions.

Ktor releases more frequently and occasionally introduces breaking changes in minor versions. The DSL is powerful but the number of configuration options grows with each release. You need to pin versions and watch release notes.

For a solo developer shipping multiple apps, Retrofit's stability reduces cognitive load. You write the same pattern in every project and move fast.

[!IMPORTANT] If you are building a library or SDK that other teams consume, Retrofit's interface-based API is easier to document and version. If you are building a single app with custom networking needs, Ktor's flexibility may save you time.

When to Choose Which

Choose Retrofit when:

  • Your app follows standard REST patterns with predictable endpoints.
  • Your team already knows OkHttp and Moshi.
  • You want maximum stability and minimal maintenance surprises.
  • You need broad community support and third-party plugin compatibility.

Choose Ktor when:

  • Your networking needs go beyond REST (WebSockets, custom protocols).
  • You want a smaller dependency footprint.
  • You prefer Kotlin-first APIs over annotation processors.
  • You already use kotlinx-serialization across the project.

I use Retrofit for most client apps because the REST pattern dominates mobile backends. I use Ktor for internal tools and WebSocket-heavy features where the builder DSL pays off.

Key Takeaways

  • Retrofit's annotated interface pattern reduces boilerplate for standard REST APIs and integrates seamlessly with OkHttp interceptors and MockWebServer.
  • Ktor's builder DSL gives you finer control per request and a smaller dependency tree, but requires more verbose setup for common patterns.
  • Pick Retrofit when stability and team familiarity matter more than flexibility. Pick Ktor when your networking needs extend beyond typical REST calls.
  • Both libraries support coroutines and kotlinx-serialization, so your data layer code looks similar regardless of the client you choose.
  • Write a thin repository wrapper around whichever client you pick, so you can swap implementations later without touching your domain layer.
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