Using Ktor In Jetpack Compose
Yes, I am late to the party. There are plenty of articles online explaining how to use Ktor in an Android application. I visited some of them while trying to learn how to actually use Ktor. But what I experienced was that since Ktor keeps progressing (and breaking things with each new major version), not all articles are up to date. Which caused confusion on my end when trying to learn things and also led to a bit of frustration when things didn’t work as was written.
Furthermore, some articles only presented a very simple use case and did not cover the whole cycle of:
Setup → Instantiation of Ktor → Making a request → Deserializing the received response
Due to this, I have decided to concentrate as much information as possible (while still maintaining an easy enough to read article) that shows how to work with Ktor.
⚠️Disclaimer: We will be showing how to use Ktor on the client (if it wasn’t apparent) and we will be using Ktor version 2.3.12
Setup - Ktor
Ktor is separated into several libraries, but regardless of what you want to do, you will need to add these two dependencies (at the very least):
dependencies {
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("io.ktor:ktor-client-android:2.3.12")
}
The core dependency, as the name implies, holds the main client functionality, while the second dependency is the ktor engine. An engine in ktor is responsible for handling the network requests and there are different variants of engines for different platforms. Since we are developing on Android, we need the Android engine.
Since we want to also work with JSON in our response and to deserialize it, we will add the following package:
dependencies {
implementation("io.ktor:ktor-client-core:2.3.12")
implementation("io.ktor:ktor-client-android:2.3.12")
implementation("io.ktor:ktor-client-content-negotiation:2.3.12") //<-- This
}
The Content Negotiation dependency is responsible for serializing/deserializing the data into a specific format (I.E. JSON)
More on serialization in Kotlin and Jetpack Compose later in this article
Now we can start by creating our HttpClient.
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
val myHttpClient = HttpClient(Android)
Pretty simple, right? We basically instantiated a ktor HttpClient and passed as an argument the specific Engine type we want. Since we want to make our HttpClient also handle JSON, we need to install a configuration to this client. This object is known as the HttpClientConfig and allows us to configure how our HttpClient will work in many ways.
import io.ktor.client.HttpClient
import io.ktor.client.engine.android.Android
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.serialization.kotlinx.json.json
val myHttpClient = HttpClient(Android) {
install(ContentNegotiation) {
json()
}
}
Here, we are telling ktor to install the ContentNegotiation plugin and we are configuring this plugin to work with the JSON serializer. There is a lot more that you can configure inside the HttpClientConfig. For example, you can set the request timeout in milliseconds by installing the HttpTimeout extension:
val myHttpClient = HttpClient(Android) {
install(HttpTimeout) {
requestTimeoutMillis = 10000
}
install(ContentNegotiation) {
json()
}
}
Setup - Serialization
I have covered setting up serialization in Jetpack Compose in a previous article and you can read it here:
But if I would have to condense that article down into a few lines, it would be these:
- Understand which Jetpack Compose version you are working with
- This will tell you which Kotlin version you need to work with
- Then you must find a corresponding kotlin-serialization library version that matches the Kotlin version you found in step #2
Once you have all that figured out, go to your project level build.gradle.kts file you put the following in the plugins block:
plugins {
...
kotlin("jvm") version "1.5.0"
kotlin("plugin.serialization") version "1.5.0"
}
And in your application level build.gradle.kts file, modify the plugins block and the dependencies block:
plugins {
...
id("org.jetbrains.kotlin.plugin.serialization")
}
...
dependencies {
...
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.12")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.0")
}
Serialization & Deserialization
With all the setup behind us, it is time to deal with creating our data class and converting a response to it. Let’s imagine we have a server that returns data about a car. Meaning, it can return it’s make, model, year of production and so on. So, let’s create a data class that models this data:
@Serializable
data class CarDetails(
@SerialName("car_make")
val make: String,
@SerialName("car_model")
val model: String,
@SerialName("year_of_production")
val productionYear: Int
...
)
Regardless of how you will structure your application, you will have a class responsible for making the requests. Inside of this class, we will instantiate our HttpClient and use it. Below is an example of a Get request:
val httpResponse: HttpResponse = try {
myHttpClient.get {
url {
protocol = URLProtocol.HTTPS
host = "www.google.com"
encodedPath = "path/file.html"
}
}
} catch (e: Exception) {
//Handle exception in request
}
After specifying the type of request using the get block, which holds a HttpRequestBuilder object. We then create a url block, which holds a URLBuilder object. In the code snippet above, you can see that it is possible to set the protocol (http/https), the host and the query parameters of the GET request. Having said that, there is more that can be configured and you can also choose to just pass the entire endpoint, without settings the various URL components separately:
val httpResponse: HTTPResponse = myHttpClient.get("https://www.domain.com/path/file.html")
Executing a POST request is just as simple:
val httpResponse: HTTPResponse = myHttpClient.post("https://www.domain.com/path/file.html") {
setBody("body content goes here")
}
But how do we handle the response?
First, the HttpResponse object has a status field that can let us know if the response has a status of 200 or not. Once we know that, we can cast the response’s body as the data class we created.
val httpResponse: HTTPResponse = myHttpClient.get("https://www.domain.com/path/file.html")
when (httpResponse.status.value) {
in 200..299 -> {
val carDetails = httpResponse.body() as CarDetails
}
else -> {
// Handle various server errors
}
}
If you would like to see a full code example of everything discussed here, you can go here to check it out:
If you would like to read other articles I have written, you can check them out here: