Skip to content
Go back

Ktorfit + Kotlin Multiplatform: Retrofit-like Networking for KMP Apps

by KMP Bits

KMP Bits Cover

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, not kspCommonMain, 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


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:

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.


Share this post on:

Previous Post
🚨 iOS 26’s Liquid Glass Is a Game-Changer for Kotlin Multiplatform — And a Wake-Up Call for Flutter
Next Post
Flutter vs Kotlin Multiplatform: KMP with SwiftUI — Native Code with Kotlin Brain