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
Feature | Ktor | Retrofit |
---|---|---|
Language Style | Pure Kotlin | Java-style annotations |
Coroutine Support | ✅ Native | ✅ Needs Coroutine Adapter |
Multiplatform Support | ✅ Yes | ❌ Android-only |
Ease of Use | Moderate (some learning) | Easy (beginner-friendly) |
Customization | High (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 lineimplementation("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")
Enable Kotlin serialization in build.gradle.kts
Add in your build.gradle.kts (Project):
kotlin("plugin.serialization") version "1.9.0"
🧩 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
🧠 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.