Originally published on Medium
Bring Retrofit’s simplicity to Kotlin Multiplatform with Ktorfit — type-safe networking that just works everywhere.
📌 Introduction
If you’ve worked with Retrofit on Android, you know how straightforward and powerful it is for API calls. But when stepping into Kotlin Multiplatform (KMP), that same ease can feel elusive — until you discover Ktorfit.
Ktorfit brings the familiar Retrofit-style annotations and structure into the multiplatform realm, built on top of Ktor and powered by Kotlin Symbol Processing (KSP). It promises type-safe, multiplatform networking that works across Android, iOS, desktop, and more.
But here’s the kicker: while the library is great, the official docs don’t always cover the quirks you’ll face in a real KMP setup — especially beyond Android.
In this article, I’ll walk you through a Compose Multiplatform demo app using Ktorfit with the JSONPlaceholder API to simulate a full CRUD experience.
The demo app is on GitHub.
🚀 Why Ktorfit?
If you’ve used Retrofit, you’ve likely come to love its simplicity — just define an interface, annotate your endpoints, and let the library do the rest.
That’s where Ktorfit steps in.
Ktorfit is a multiplatform networking library inspired by Retrofit, built on top of Ktor Client and powered by KSP. It brings a familiar developer experience to KMP while embracing shared logic and minimal boilerplate.
✅ Retrofit-like API, Shared Across Platforms
Ktorfit lets you define your API using annotations like @GET
, @POST
, @Path
, and @Body
, just like Retrofit.
interface ApiService {
@GET("todos")
suspend fun getTodos(): List<Post>
}
No manual HttpClient.request()
calls. Just clean interfaces that work across platforms.
🔁 Built on Ktor = Full Flexibility
Under the hood, Ktorfit uses Ktor, so you still get full control — custom engines, interceptors, logging, timeouts, and more.
Want centralized error handling or custom auth? You still can.
🌐 Multiplatform Support, Out of the Box
Ktorfit supports Android, iOS, JVM, JS, and more. Define your API once and reuse it everywhere: Compose Multiplatform, SwiftUI, or even Web.
🧰 Simple Setup — Once You Know the Gotchas
The setup might feel tricky at first (KSP + KMP + serialization), but once done, it’s smooth sailing. This article is your shortcut to a pain-free integration.
🛠️ Setting Up Ktorfit in a KMP Project
Let’s walk through the steps to integrate Ktorfit into a Kotlin Multiplatform project.
1️⃣ Add Ktorfit and KSP to Your libs.versions.toml
[versions]
ktorfit = "2.5.1"
ksp = "2.1.0-1.0.29"
serialization-json = "1.8.1"
ktor = "3.1.3"
[libraries]
ktorfit = { module = "de.jensklingenberg.ktorfit:ktorfit-lib", version.ref = "ktorfit" }
ktorfit-compiler = { module = "de.jensklingenberg.ktorfit:ktorfit-ksp", version.ref = "ktorfit" }
serialization-json = { module = "org.jetbrains.kotlinx:kotlinx-serialization-json", version.ref = "serialization-json" }
content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
kotlinx-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
[plugins]
ktorfit = { id = "de.jensklingenberg.ktorfit", version.ref = "ktorfit" }
kotlinSerialization = { id = "org.jetbrains.kotlin.plugin.serialization", version = "2.0.0" }
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }
2️⃣ Apply Plugins in build.gradle.kts
plugins {
alias(libs.plugins.ktorfit)
alias(libs.plugins.kotlinSerialization)
alias(libs.plugins.ksp)
}
3️⃣ Configure Dependencies + KSP
kotlin {
sourceSets {
commonMain.dependencies {
implementation(libs.ktorfit)
implementation(libs.serialization.json)
implementation(libs.content.negotiation)
implementation(libs.kotlinx.json)
}
}
}
dependencies {
add("kspCommonMainMetadata", libs.ktorfit.compiler)
add("kspAndroid", libs.ktorfit.compiler)
add("kspIosSimulatorArm64", libs.ktorfit.compiler)
add("kspIosX64", libs.ktorfit.compiler)
add("kspIosArm64", libs.ktorfit.compiler)
}
💡 Use
kspCommonMainMetadata
, notkspCommonMain
, or KSP won’t generate anything.
4️⃣ Create Your Data Models
@Serializable
data class TodoDto(
val userId: Int,
val id: Int,
val title: String,
val completed: Boolean
)
5️⃣ Define the API Interface
interface ApiService {
@GET("todos")
suspend fun getTodos(): List<TodoDto>
@POST("todos")
@FormUrlEncoded
suspend fun addTodo(
@Field("userId") userId: Int = 1,
@Field("id") id: Int = Random.nextInt(),
@Field("title") title: String,
@Field("completed") completed: Boolean,
): TodoDto
@PUT("todos/{id}")
@FormUrlEncoded
suspend fun updateTodo(
@Path("id") id: Int,
@Field("userId") userId: Int = 1,
@Field("title") title: String,
@Field("completed") completed: Boolean,
): TodoDto
@DELETE("todos/{id}")
suspend fun deleteTodo(@Path("id") id: Int)
}
6️⃣ Provide Dependencies (with Koin Annotations, Optional)
@Module
class NetworkModule {
@Single
fun provideHttpClient(): HttpClient = HttpClient {
install(ContentNegotiation) {
json()
}
}
@Single
fun provideKtorfit(client: HttpClient): Ktorfit =
Ktorfit.Builder()
.baseUrl(Constants.BASE_URL)
.httpClient(client)
.build()
@Single
fun provideApiService(ktorfit: Ktorfit): ApiService =
ktorfit.createApiService()
}
✅ If you’re new to Koin Annotations, I wrote a full breakdown in this article.
🧪 The Compose Multiplatform Demo
Now that we’ve set up Ktorfit in our shared module, it’s time to build the UI using Compose Multiplatform.
🔁 Shared ViewModel (Ktorfit + Koin)
@KoinViewModel
class TodoViewModel(
private val repository: TodoRepository
) : ViewModel() {
private val _state = MutableStateFlow(TodoState())
val state: StateFlow<TodoState> = _state.asStateFlow()
init {
getTodos()
}
private fun getTodos() = viewModelScope.launch {
repository.getTodos().collectLatest { response ->
_state.update {
it.copy(todoListState = response)
}
}
}
}
🖼️ Composable UI
@Composable
fun App() {
val viewModel = koinViewModel<TodoViewModel>()
val state by viewModel.state.collectAsState()
MaterialTheme {
Scaffold(
topBar = { TopAppBar(title = { Text("Todo List") }) },
floatingActionButton = {
FloatingActionButton(onClick = { viewModel.onAction(TodoAction.ShowAddUpdateDialog()) }) {
Icon(Icons.Default.Add, contentDescription = "Add Todo")
}
}
) { padding ->
when (val result = state.todoListState) {
is ResponseState.Loading -> Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
is ResponseState.Error -> Text(result.message)
is ResponseState.Success -> {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
contentPadding = padding,
verticalArrangement = Arrangement.spacedBy(10.dp)
) {
items(result.data) { todo ->
TodoItem(
todo = todo,
onCheckChanged = {
viewModel.onAction(TodoAction.UpdateTodoCheck(todo))
},
onDelete = {
viewModel.onAction(TodoAction.DeleteTodo(todo.id))
}
)
}
}
}
}
}
}
}
🧯 Troubleshooting Tips
- No generated code?
→ Check if you’re using
kspCommonMainMetadata
, notkspCommonMain
. - Ktor client fails?
→ Ensure
ContentNegotiation
is installed and models are@Serializable
. - DI issues with Koin?
→ Confirm you’re using the correct Koin Annotations (
@Module
,@Single
).
Ktorfit brings a breath of Retrofit-like simplicity to Kotlin Multiplatform development — with shared networking, annotations, and full flexibility via Ktor. And once it’s wired up, calling APIs becomes seamless — just like on Android, but now multiplatform-ready.
📚 Official Documentation
For the latest updates and full API reference, check out the official Ktorfit docs here: https://ktorfit.jensklingenberg.de/
🙌 Let’s Keep the Conversation Going
Liked this article? Found Ktorfit interesting? Here’s how you can take the next step:
- ⭐ Star the GitHub repo and try the demo yourself
- 🧠 Read the related article on Koin Annotations to understand the DI setup behind the scenes
- 🛎️ Follow KMP Bits for more Kotlin Multiplatform deep dives, tips, and honest takes
And if you hit a wall trying to get Ktorfit working, I’ve probably been there. Drop a comment or DM — I’d love to help.