Exploring Ktor: An Alternative To Retrofit For HTTP Requests In Android
A Guide Through Integrating The Ktor Client Into Your Native Android Project

Although Ktor has already won great popularity among developers for Kotlin Multiplatform Projects, the leading framework used for processing HTTP requests is still Retrofit.
However, looking at alternative solutions to your current tech stack never hurts. So in this article, we will look at how to set up the Ktor client in your native Android project, handle HTTP requests and finally discuss whether you should use Ktor over Retrofit for your current native Android project.
1 Gradle Setup
To be able to properly configure and use our Ktor client, we first need to enrich our Gradle configuration with a few dependencies.
1.1 Client dependency
The first thing we need to do is to add the dependency for the Ktor Client Android engine.
dependencies {
implementation "io.ktor:ktor-client-android:2.2.3"
}1.2 Logging
The Logging plugin allows us to log our HTTP calls. To be later able to add it to our configuration, add the following to your project-level build.gradle file:
dependencies {
implementation "io.ktor:ktor-client-logging:2.2.3"
}1.2 JSON Serialization/Deserialization
For serialization, we need to add the Kotlin serialization plugin to our project. To do so, add the following to your project-level build.gradle file:
buildscript {
ext.your_kotlin_version = '1.8.0'
repositories { mavenCentral() }
dependencies {
classpath "org.jetbrains.kotlin:kotlin-serialization:$your_kotlin_version"
}
}Next, we need to add the Ktor Content Negotiation to our app module build.gradle dependencies.
For serialization and deserialization of JSON data, we have the option to use GSON, Jackson, or kotlinx.serialization. In our case, we will use kotlinx.serialization.
We will therefore add the content negotiation and kotlinx.serialization dependency to the dependencies block as you can see in the following code snippet:
dependencies {
implementation "io.ktor:ktor-client-content-negotiation:2.2.3"
implementation "io.ktor:ktor-serialization-kotlinx-json:2.2.3"
}1.3 Setup The Base URL
A common thing to do is to provide the base URL of our respective API to the build configuration, so it changes depending on our respective build flavors.
Let’s assume we have a dev and a prod flavor. One for development and the other for production.
To add a new field to the build configuration for each of those flavors, we can add a new build config field to our productFlavors section in the app module build.gradle file.
android {
..
flavorDimensions("app")
productFlavors {
dev {
dimension = "app"
applicationId "dev.com.your.project"
buildConfigField 'String', 'BASE_URL', '"https://dev.yourapi.com"'
}
prod {
dimension = "app"
applicationId "com.your.project"
buildConfigField 'String', 'BASE_URL', '"https://yourapi.com"'
}
}
}
}3 Creating the Client Configuration
Now that we have our Gradle setup in place, let’s proceed with the actual setup of the HttpClient.
Just like we need to configure a OkHttpClient and create Retrofit instance with Retrofit, we need to configure a HttpClient for a respective engine.
Because we are in an Android project, of course, we need to configure the Android engine.
fun createKtorHttpClient(): HttpClient = HttpClient(Android) {
.. // Our installations
}Now inside the HttpClientConfig block, we can add (install) the plugins we want to configure for our HttpClient.
3.1 Timeout
The first thing we want to configure is the connection time-out. That is the maximum time a connection with a server stays active. The configuration time gets served in milliseconds.
In the example below, we set both, connectTimeout and socketTimeout to 30 milliseconds.
fun provideKtorHttpClient(): HttpClient = HttpClient(Android) {
engine {
connectTimeout = 30_000
socketTimeout = 30_000
}
.. // Other configurations
}3.2 JSON Serialization/Deserialization
Next, we add the ContentNegotiation. Inside the configuration block, we enable the json configuration. This way we can automatically serialize and deserialize requests and responses respectively.
fun provideKtorHttpClient(): HttpClient = HttpClient(Android) {
.. // Other configurations
install(ContentNegotiation) {
json(Json {
prettyPrint = true
isLenient = true
ignoreUnknownKeys = true
})
}
}3.3 Logging
We want to be able to log our HTTP calls. Therefore we add the Logging plugin to the configuration
fun provideKtorHttpClient(): HttpClient = HttpClient(Android) {
.. // Other configurations
install(Logging) {
logger = object : Logger {
override fun log(message: String) {
// Log the message with the framework of your choice
}
}
level = LogLevel.ALL
}
}To also be able to log the responses we can add the following:
fun provideKtorHttpClient(): HttpClient = HttpClient(Android) {
.. // Other configurations
install(ResponseObserver) {
onResponse { response: HttpResponse ->
// Log the response with the logging framework of your choice
}
}
}3.4 Default Request Configuration
We can also configure default values for all requests we execute with our HttpClient. For example, we can provide some default headers that should be present in all requests.
But that’s not all, here is also the place where we can finally set up our base URL that we previously configured in section 1.3 of this article.
fun provideKtorHttpClient(): HttpClient = HttpClient(Android) {
.. // Other configurations
install(DefaultRequest) {
url(BuildConfig.BASE_URL)
header(HttpHeaders.ContentType, ContentType.Application.Json)
}
}4 Creating Our First Request
Now that we have our configuration in place, let’s take a look at an example of how we can communicate with a sample API.
Making a request is quite simple. Let’s take an ordinary ViewModel as an example, where we want to execute our request.
As an example, we use open accessible joke API. This API randomly generates a joke and requires no authentication.
We define a DTO for the response like the following data class shows:
import kotlinx.serialization.Serializable
@Serializable
data class JokeDTO(
val joke: String? = null,
val answer: String? = null,
val setup: String? = null,
val delivery: String? = null
)Note that we need to add the @Serialization annotation from the kotlinx.serialization plugin.
To actually execute the GET request to this API, we can call get() at an instance of our Ktor HttpClient.
class JokeViewModel(
private val httpClient: HttpClient
) : ViewModel() {
fun loadJoke() {
viewModelScope.launch(Dispatchers.IO) {
val jokeDTO: JokeDTO = httpClient
.get()
.body()
.. // further process the jokeDTO
}
}
}In the example above, we provide an instance of our previously configured HttpClient to the JokeViewModel via its constructor.
Assuming that we provided the joke API URL as our base URL, after calling get() we now can directly convert the body of the HttpResponse to the respective DTO we expect from a request. In our case the JokeDTO.
Note that the call had to be executed in a coroutine scope.
Conclusion
If you’d ask me, “should I use Ktor over Retrofit”? I could only give you the unsatisfying answer: “it depends”.
Ktor comes with the benefit that it’s fully built on Kotlin, has built-in support for WebSockets, authorization can be greatly further customized with various components.
But the major benefit is of course that Ktor, other than Retrofit, can be used in a Kotlin Multiplatform project as an HTTP Client. So if you plan someday to migrate your project to a Kotlin Multiplatform Mobile project, the migration process would be way easier.
On the other hand, while Retrofit is still the industry standard, Ktor also has no clear direct benefit if you just use it as a traditional HTTP Client for REST communication which is probably required in most projects.
In the end, it comes down to your personal preference. If you’re starting a new Android project, consider giving Ktor a try. But if you have an older project, the effort required to migrate from one framework to another may not be worth it.
I hope you had some takeaways, clap if you liked my article, make sure to subscribe to get notified via e-mail, and follow for more!






