Typical Wireframe

Making an http request using Ktor client

Mubarak Native
6 min readJun 23, 2024

--

In this article, we are going to learn how to use Ktor Client for making http requests in Android, also why we need to use ktor, and explore some customization in it and more Let’s begin.

What is Ktor ??

Ktor is a Kotlin based framework used for Server-Side and also for Client-Side applications backed by Coroutine, so it achieves asynchronous in nature. In this article we use Ktor as a client to make a http request just like Retrofit (OkHttp).

Why Ktor ??

As you might ask why we need a Ktor, we already have a Retrofit, Volley to make an HTTP request.

Yes, there are already many libraries to make an HTTP request for us, such as: Retrofit, but there are Written in Java, so we can’t use it on the Kotlin Multiplatform project. KMM is the most prominent powerful technology to build modern cross-platform applications. Recently, Google has also supported the KMP for Sharing business logic in android apps. Ktor is not only for KMM, it is backed by Kotlin Coroutines, so we have a rich number of modern features and asynchronous support.

How to work with Ktor Client ??

To customize the ktor based on features that we want, Ktor has 2 important ways to configure it.

  1. Engine
  2. Plugin

Engine

Ktor works on different platforms, such as Android, JVM, Kotlin Native and JS,. For making network requests for these platforms, Ktor requires a platform specific Engine for Android. It is ‘Android’, JVM for ‘Java’ and other

Note: For some platforms may not support some features, ex: for Android it doesn’t support HTTP/2 and WebSocket for now.

Plugin

For some additional features that might not be available out of the box, we can use a plugin such as: Logging or Serialization

Now we have understood the basic terminology of the Ktor Client. Now let’s get into a real world demo. In this demo we use kotlinx.Serialization library to serialize our json into Kotlin objects.

What do we build ?

In this demo we build a simple app that fetches (GET) posts from a dummy api named: JsonPlaceHolder. In this demo we also use hilt Dependency injection to construct our dependencies. Obviously, we build UI using compose.

Demo Application

Let’s begin by adding the necessary dependencies for Ktor, Serialization and for Hilt

libs.versions.toml

[versions]

ktorClientAndroid = "2.3.12"
ktorClientCore = "2.3.12"
ktorClientLogging = "2.3.12"
kotlinxSerializationJson = "1.6.3"
ktorSerializationKotlinxJson = "2.3.12"

[libraries]

# Hilt (Optional)
hilt-android = { module = "com.google.dagger:hilt-android", version.ref = "daggerVersion" }
hilt-android-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "daggerVersion" }
androidx-hilt-navigation-compose = { module = "androidx.hilt:hilt-navigation-compose", version.ref = "hiltNavigationCompose" }

# Ktor (Required)
ktor-client-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktorClientCore" }
kotlinx-serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "kotlinxSerializationJson" }
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktorClientAndroid" }
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktorClientCore" }
ktor-client-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktorClientLogging" }
ktor-serialization-kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktorSerializationKotlinxJson" }
# ViewModel (Optional)
androidx-lifecycle-viewmodel-compose = { module = "androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "lifecycleViewmodelKtx" }
androidx-lifecycle-viewmodel-ktx = { module = "androidx.lifecycle:lifecycle-viewmodel-ktx", version.ref = "lifecycleViewmodelKtx" }

[plugins]

# kotlinX-Serialization (Required)
kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" }
kotlinSymbolProcessing ={ id = "com.google.devtools.ksp", version.ref = "kspVersion"} # ksp for hilt
daggerHilt = { id = "com.google.dagger.hilt.android", version.ref = "daggerHiltVersion"}

build.gradle.kts(Project Level)

// ...
alias(libs.plugins.kotlinSymbolProcessing)
alias(libs.plugins.daggerHilt) apply false
// ...

build.gradle.kts(Module:app)

plugins {
// ...
alias(libs.plugins.kotlin.serialization)
alias(libs.plugins.kotlinSymbolProcessing)
alias(libs.plugins.daggerHilt)
// ...
}

dependencies {

// Compose Navigation
implementation(libs.androidx.hilt.navigation.compose)

// ViewModel
implementation(libs.androidx.lifecycle.viewmodel.ktx)
implementation(libs.androidx.lifecycle.viewmodel.compose)

// Hilt (Dependency Injection)
implementation(libs.hilt.android)
ksp(libs.hilt.android.compiler)

implementation(libs.ktor.client.core) // Ktor-Core
implementation(libs.ktor.client.android) // Ktor-Engine

implementation(libs.kotlinx.serialization.json) // KotlinX Serialization (Convert JSON response to Kotlin Objects)
implementation(libs.ktor.serialization.kotlinx.json) // Ktor- To work with Serialization

implementation(libs.ktor.client.logging) // Logging (Optional)

implementation(libs.ktor.client.content.negotiation) // Serialization
}

We added the required dependencies. Hmm, that’s a lot. Let’s proceed to creating an application

Step: 1

Let’s start with defining the Model class for our api response just like what we did for Retrofit. Just a small change annotate this model class as @Serializable to tell the serializable library to serialize this class.

import kotlinx.serialization.Serializable

@Serializable
data class Posts( // replica of the json response
val userId: Int,
val id: Int,
val title: String,
val body: String
)

Step: 2

Define the api Interface for HTTP methods we are using in this app. In our case, it’s very simple to just (GET) fetch the posts from the server.

interface PostsApi {
suspend fun getPosts(): List<Posts> // GET
}

Step: 3

Let’s also create an implementation of this interface. This step is slightly different from what we do for retrofit. In Retrofit, we configure the api within the interface such as: @GET(”/path”) and @Query for query param. In Ktor, we create an implementation

class PostApiImpl(
private val client: HttpClient // helps to make http request and handle responses
) : PostsApi {
override suspend fun getPosts(): List<Posts> {
return client.use { // for single request use 'use' it automatically release the resources and calls close
it.get{ // get method
url {
protocol = URLProtocol.HTTPS
host = "jsonplaceholder.typicode.com"
path("/posts")
}
}
}.body()
}
}

For query parameters, configure it on url lambda ex: parameters.append(“token”, “abc123”) Key, value for post http method use client.post(// url) and configure it on its lambda just like this

client.post("https://jsonplaceholder.typicode.com") {
contentType(ContentType.Application.Json)
setBody(Posts(userId = 3, id = 2,title = "Ktor",body = "Ktor is awesome"))
}

Now is the time for dependency injection. I don’t go deeper into this because our focus is only on Ktor. If you want to know how to implement DI in our app, please comment on this article. I will try to puslish it.

NetworkModule.kt

@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

@Singleton
@Provides
fun provideHttpClient():HttpClient{
return HttpClient(Android){
install(ContentNegotiation){
json()
}
install(Logging){
level = LogLevel.ALL
}
}
}

@Singleton
@Provides
fun providePostApi(client: HttpClient): PostsApi {
return PostApiImpl(
client
)
}
}

Let’s go through the HttpClient function. The first parameter of this function takes an engine. In this case, we are on Android, so pass `Android` as an engine. As I previously mentioned, you need to add dependencies on a specific platform for the engine. The next parameter is userConfig. It is a lambda to configure it in this block. We declare our plugins and configure them like this.

HttpClient(Android /* engine */) { // userConfig lambda
install(ContentNegotiation) { // adding our plugins in install function and config on its lambda
json() // we use json becuase our server return json response
}
install(Logging) { // adding our logging plugin (Optional) just like Interceptor in OkHttp for log
level = LogLevel.ALL
}
// add more plugin as needed using install
}

Don’t forgot to create a Application class and annotate it with @HiltAndroidApp and define it on AndroidManifest.xml

Let’s also create a ViewModel

data class PostsUiState(
val posts:List<Posts> = emptyList()
)

@HiltViewModel
class MainViewModel @Inject constructor(
private val postsApi: PostsApi
):ViewModel() {

private val _uiState: MutableStateFlow<PostsUiState> = MutableStateFlow(PostsUiState())
val uiState: StateFlow<PostsUiState> = _uiState.asStateFlow()

init {
viewModelScope.launch {
_uiState.update {
it.copy(
posts = postsApi.getPosts() // api response
)
}
}
}
}

Go to our MainActivity and consume our api. Also, don’t forget to annotate our MainActivity. With @AndroidEntryPoint

@Composable
fun App(modifier: Modifier = Modifier,viewModel: MainViewModel = hiltViewModel()) { // call this composable on setContent{}
val uiState = viewModel.uiState.collectAsStateWithLifecycle()

LazyColumn(modifier = modifier) {
items(uiState.value.posts) {
Column(
modifier = Modifier.padding(horizontal = 16.dp)
) {
Text(text = it.title, style = MaterialTheme.typography.titleMedium)
Spacer(modifier = Modifier.height(12.dp))
Text(text = it.body, style = MaterialTheme.typography.bodySmall)
}
}
}
}

When we run the app, it successfully gets the posts from the server.

That’s it we learned what ktor is, why we use ktor and also learned how to configure the ktor with adding plugins and engines for complete source code of this application, checkout the repository down below.

If you like this article and learned something new, please considering 👏applause to this content and share it with your friends, because creating this types of content takes me many hours 😓 to design a thumbnail image and arrange this content, such a way as you many of them easily understand this topic better. I will see you on my next interesting topic.

Signing off, Mubarak Native

--

--

Mubarak Native

I am Mubarak "Native Android Developer" Focus on Clean and Minimal Code (simply love to write clean code)