How to Use Ktor in a Jetpack Compose App

How to Use Ktor in a Jetpack Compose App

In the world of modern Android development, Jetpack Compose is quickly becoming the go-to UI toolkit for building beautiful and reactive interfaces using Kotlin. But once your UI is ready, the next big question is: how do you fetch data from APIs?

Most Android developers default to Retrofit, but there’s another powerful, Kotlin-native alternative: Ktor.

How to Use Ktor in a Jetpack Compose App

In this article, we’ll explore:
  • What is Ktor?
  • Why it fits perfectly with Jetpack Compose
  • How it compares to Retrofit
  • How to use Ktor in a real-world Compose project

🚀 What is Ktor?

Designed with modern Kotlin principles in mind, Ktor offers a sleek and modular way to handle HTTP in your app — and yes, it comes from the same brilliant minds at JetBrains who brought us Kotlin.
  • HTTP clients for apps (like Android, iOS)
  • HTTP servers for backends (like APIs)
Unlike some traditional Android networking libraries, Ktor is built entirely in Kotlin. That means fewer compatibility issues, less boilerplate, and smooth coroutine integration.

🤔Benefits of Using Ktor for Network Calls in Jetpack Compose Applications

If you're building a Jetpack Compose app, you already value Kotlin's clean syntax and modern patterns. Here's why Ktor fits in naturally:

✅ 1. Kotlin-First Approach

Retrofit relies on annotations and reflection — concepts borrowed from Java. Ktor, on the other hand, is written purely in Kotlin and offers a more idiomatic style of coding.

✅ 2. Native Coroutine Support

Ktor works seamlessly with suspend functions, making it perfect for ViewModel logic in Compose apps. No adapters, no RxJava — just clean suspend fun calls.

✅ 3. Lightweight and Modular

Ktor doesn’t come bundled with unnecessary components. You pick exactly what you need: JSON, logging, timeout handling, etc. That keeps your APK smaller and more efficient.

✅ 4. Multiplatform Ready

Planning to reuse your logic in iOS or desktop apps using Kotlin Multiplatform? Ktor is already compatible across platforms.

🔁 Ktor vs Retrofit – A Quick Comparison

FeatureKtorRetrofit
Language StylePure KotlinJava-style annotations
Coroutine Support✅ Native✅ Needs Coroutine Adapter
Multiplatform Support✅ Yes❌ Android-only
Ease of UseModerate (some learning)Easy (beginner-friendly)
CustomizationHigh (flexible pipeline)Medium

🛠️ How to Use Ktor in a Jetpack Compose App

Let’s say you’re building a dictionary app. Here's a simple example using Ktor to fetch word definitions. We’ll build a mini Dictionary App using Free Dictionary API where:
  • User enters a word
  • Ktor fetches meaning from the API
  • Result is shown in a clean Jetpack Compose UI

✅ Tech Stack

  • Jetpack Compose
  • Ktor Client
  • ViewModel + StateFlow
  • Kotlinx Serialization
  • Material 3 Theme

📦 Step-by-Step Project Code

🧱 1. build.gradle dependencies

id("org.jetbrains.kotlin.plugin.serialization") // 👈 Add this line  
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.9.1")
// Ktor
implementation("io.ktor:ktor-client-core:2.3.2")
implementation("io.ktor:ktor-client-cio:2.3.2")
implementation("io.ktor:ktor-client-content-negotiation:2.3.2")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.2")

// Serialization
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
build.gradle dependencies(Module:app)
build.gradle dependencies(Module:app)



Enable Kotlin serialization in build.gradle.kts
Add in your build.gradle.kts (Project):
kotlin("plugin.serialization") version "1.9.0"
build.gradle dependencies Project

🧩 2. Data Model (WordResponse.kt)

package com.example.ktordictionaryapp

import kotlinx.serialization.Serializable

@Serializable
data class WordResponse(
val word: String,
val meanings: List<Meaning>
)
@Serializable
data class Meaning(
val partOfSpeech: String,
val definitions: List<Definition>
)
@Serializable
data class Definition(
val definition: String,
val example: String? = null
)

🌐 3. Ktor Client (DictionaryApi.kt)

package com.example.ktordictionaryapp

import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.client.plugins.contentnegotiation.ContentNegotiation
import io.ktor.client.request.get
import io.ktor.serialization.kotlinx.json.json
import kotlinx.serialization.json.Json
import io.ktor.client.call.body

object DictionaryApi {

private val client = HttpClient(CIO) {
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
isLenient = true
})
}
}

suspend fun getWordMeaning(word: String): List<WordResponse> {
return client
.get("https://api.dictionaryapi.dev/api/v2/entries/en/$word")
.body() // Deserialize to List<WordResponse>
}
}

🧠 4. ViewModel (DictionaryViewModel.kt)

package com.example.ktordictionaryapp

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.ktordictionaryapp.DictionaryApi.getWordMeaning
import kotlinx.coroutines.launch

class DictionaryViewModel : ViewModel() {
var word by mutableStateOf("")
var isLoading by mutableStateOf(false)
var result by mutableStateOf<List<WordResponse>>(emptyList())
var error by mutableStateOf<String?>(null)

fun search() {
viewModelScope.launch {
isLoading = true
error = null
try {
result = getWordMeaning(word)
} catch (e: Exception) {
error = "Word not found or network error"
result = emptyList()
} finally {
isLoading = false
}
}
}
}

🎨 5. UI Screen (MainScreen.kt)

package com.example.ktordictionaryapp

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.systemBarsPadding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun DictionaryScreen(viewModel: DictionaryViewModel = viewModel()) {
val word = viewModel.word
val result = viewModel.result
val isLoading = viewModel.isLoading
val error = viewModel.error

Column(
modifier = Modifier.systemBarsPadding()
.fillMaxSize()
.padding(16.dp)
) {

OutlinedTextField(
value = word,
onValueChange = { viewModel.word = it },
label = { Text("Enter a word") },
modifier = Modifier.fillMaxWidth()
)

Spacer(modifier = Modifier.height(8.dp))

Button(
onClick = viewModel::search,
modifier = Modifier.run { align(Alignment.End) }
) {
Text("Search")
}

Spacer(modifier = Modifier.height(16.dp))

when {
isLoading -> CircularProgressIndicator()
error != null -> Text(error, color = Color.Red)
result.isNotEmpty() -> {
LazyColumn {
items(result) { wordResponse ->
Text(
text = wordResponse.word,
style = MaterialTheme.typography.titleLarge
)
wordResponse.meanings.forEach { meaning ->
Text(
text = "- ${meaning.partOfSpeech}",
style = MaterialTheme.typography.labelLarge
)
meaning.definitions.forEach {
Text("• ${it.definition}")
it.example?.let { ex -> Text("Example: $ex") }
}
}
Spacer(modifier = Modifier.height(16.dp))
}
}
}
}
}
}

🏁 6. MainActivity.kt

package com.example.ktordictionaryapp

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import com.example.ktordictionaryapp.ui.theme.KtorDictionaryAppTheme

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
enableEdgeToEdge()
setContent {
KtorDictionaryAppTheme {
/*Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Greeting(
name = "Android",
modifier = Modifier.padding(innerPadding)
)
}*/

DictionaryScreen()
}
}
}
}

✅ Output

  • User types a word like "happy"
  • Clicks Search
  • Ktor makes HTTP GET call to API
  • Shows definitions and examples using clean Compose UI
Dictionary app with Ktor Screenshot 1
Dictionary app with Ktor Screenshot 2

🧠 Summary

  • This Compose app uses Ktor for HTTP in a clean way.
  • You get full coroutine support and Kotlin-style networking.
  • The UI is reactive and modern — no XML, no Retrofit, no boilerplate.

Previous Post Next Post

Contact Form