Network Requests with Ktor Client in And ...

Network Requests with Ktor Client in Android Jetpack Compose

Sep 16, 2025

image

Building modern Android applications is almost impossible without network interaction. Whether you need to fetch data from servers, send information, or handle authentication, you need a reliable and convenient tool for making network requests. In the Kotlin ecosystem, Ktor Client stands out as one of the best solutions for this purpose.

  • Pure Kotlin: Seamlessly integrates with coroutines, making asynchronous code clean and readable

  • Multiplatform: Works not only on Android but also iOS, JVM, and native applications

  • Extensible: Rich plugin system for authentication, JSON serialization, logging, and much more

For comprehensive details about request handling, you can check the complete Ktor guide. In this tutorial, we’ll focus on integrating and using Ktor Client in a Jetpack Compose application for network requests.

Setting Up Dependencies

To work with Ktor in Android, you first need to add Ktor Client dependencies to your project. Open the build.gradle.kts file of your app module and add these dependencies to the "dependencies" block:

dependencies {
    // Ktor Core
    implementation("io.ktor:ktor-client-core:3.2.1")
    // Android Engine
    implementation("io.ktor:ktor-client-android:3.2.1")
    // ... other dependencies
}

Also add internet permission to your AndroidManifest.xml file:

<uses-permission android:name="android.permission.INTERNET" />

Basic Network Request Example

Let’s say we need to load some data when the app starts. As a test endpoint, we’ll use “https://google.com" to fetch the main page HTML. Here’s how to implement this in MainActivity.kt:

package com.example.httpapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Text
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsTextclass MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var text by remember { mutableStateOf("Loading...") }
            val client = HttpClient() // Create HttpClient for handling requests
            
            LaunchedEffect(true) { // Execute request when app starts
                try {
                    val response = client.get("https://google.com/") // Make request to google.com
                    text = response.bodyAsText() // Read response text
                } catch (e: Exception) {
                    text = e.localizedMessage ?: "Error occurred"
                } finally {
                    client.close() // Close the client
                }
            }
            
            Text(text = text, Modifier.padding(10.dp), fontSize = 16.sp)
        }
    }
}

The UI consists of a single Text widget that initially shows "Loading..." and then displays the HTML code received from google.com after the request completes.

First, we create an HttpClient object:

val client = HttpClient()

HttpClient functions that perform requests (like the get() function) are typically suspend functions and must be executed from other suspend functions or coroutines.

We use the built-in LaunchedEffect component, which immediately launches a coroutine when the app starts:

LaunchedEffect(true) { // Execute request when app starts
    // ... request code
}

Since various errors can occur during request execution, we wrap the request in a try..catch block.

In the try block, we make the request using the get() method, get the response in text form, and assign it to the text variable:

val response = client.get("https://google.com/") // Make request to google.com
text = response.bodyAsText() // Read response text

After request completion (successful or failed), we close the HttpClient in the finally block:

finally {
    client.close() // Close the client
}

Triggering Requests with Button Clicks

Similarly, we can use other components to initiate request execution. For example, let’s send a request when a button is pressed:

package com.example.httpapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.launchclass MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            var text by remember { mutableStateOf("") }
            // Define coroutine scope
            val coroutineScope = rememberCoroutineScope()
            
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(16.dp)
            ) {
                Button(onClick = {
                    // Launch coroutine
                    coroutineScope.launch {
                        text = "Loading..."
                        val client = HttpClient() // Create HttpClient for handling requests
                        try {
                            val response = client.get("https://google.com/") // Make request to google.com
                            text = response.bodyAsText() // Read response text
                        } catch (e: Exception) {
                            text = e.localizedMessage ?: "Error occurred"
                        } finally {
                            client.close() // Close the client
                        }
                    }
                }) {
                    Text("Download", fontSize = 16.sp)
                }
                
                Spacer(modifier = Modifier.height(24.dp))
                Text(text = text, fontSize = 16.sp)
            }
        }
    }
}

Here we define a coroutine scope using the rememberCoroutineScope function:

val coroutineScope = rememberCoroutineScope()

In the button’s click handler, we launch a coroutine to execute the request:

Button(onClick = {
    // Launch coroutine
    coroutineScope.launch {
        text = "Loading..."
        val client = HttpClient() // Create HttpClient for handling requests
        try {
            val response = client.get("https://google.com/") // Make request to google.com
            // ... rest of the code
        }
    }
})

Optimizing HttpClient Usage

While the previous request examples work fine, it’s important to note that HttpClient is a very heavy object. Creating and releasing it requires significant resources. In the example above, a new HttpClient object is created each time the button is pressed. If we need to make another request, we create another HttpClient object, and so on.

In the first example where HttpClient is created and used in LaunchedEffect, even though the HttpClient is created once, it may be recreated during recompositions.

If HttpClient is needed throughout the entire application or a set of activities, you can make HttpClient a singleton with the application’s lifecycle. This is the most efficient approach since the client is created once at app startup and used by all screens:

object AppHttpClient {
    val instance = HttpClient(Android)
}

Using ViewModel for Better Architecture

Additionally, it’s more optimal to bind HttpClient to a ViewModel: create the client when the ViewModel is initialized and close it in the onCleared() method. This ensures the client survives screen rotations and is properly closed when the screen is no longer needed.

To use ViewModel, add the corresponding dependency to build.gradle.kts:

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")

And define the following code in MainActivity.kt:

package com.example.httpapp
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import io.ktor.client.HttpClient
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsText
import kotlinx.coroutines.launch
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.lifecycle.viewmodel.compose.viewModel
import io.ktor.client.engine.android.Androidclass MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            val mainViewModel: MainViewModel = viewModel()
            
            Column(
                modifier = Modifier
                    .fillMaxSize()
                    .padding(16.dp)
            ) {
                Button(onClick = { mainViewModel.fetchData() }) {
                    Text("Download", fontSize = 16.sp)
                }
                
                Spacer(modifier = Modifier.height(24.dp))
                Text(text = mainViewModel.responseText.value, fontSize = 16.sp)
            }
        }
    }
}object AppHttpClient {
    val instance = HttpClient(Android)
}class MainViewModel : ViewModel() {
    // State for storing server response
    val responseText = mutableStateOf("")
    
    fun fetchData() {
        // Launch coroutine in ViewModelScope
        viewModelScope.launch {
            responseText.value = "Loading..."
            try {
                // Execute GET request
                val response = AppHttpClient.instance.get("https://google.com")
                responseText.value = response.bodyAsText()
            } catch (e: Exception) {
                // Handle errors
                responseText.value = "Error: ${e.message}"
            }
        }
    }
    
    override fun onCleared() {
        super.onCleared()
        // Close client when ViewModel is destroyed
        AppHttpClient.instance.close()
    }
}

This architecture provides better separation of concerns, proper lifecycle management, and more efficient resource usage for network operations in your Android Jetpack Compose applications.

¿Te gusta esta publicación?

Comprar DelphiFan Forum un café

Más de DelphiFan Forum