Koin, Ktor & Paging in KMM | Compose Multiplatform

Kotlin Multiplatform Mobile (KMM) has evolved from an ambitious idea to a stable and powerful framework, providing developers with the ability to share code across multiple platforms seamlessly. With its recent stability milestone, KMM has become a game-changer in the world of cross-platform development.
Setting the Stage: Environment Setup
- Android Studio with Kotlin Multiplatform Plugin
- Kotlin Version: 1.9.10
- Gradle Version: 8.1.1
- Any open API for demo — Here, I’ve used Internshala listing API to populate UI.
Note: Using already open listing API from Internshala, no bad practices followed here :)
Setting up Koin and Ktor —
- Add both dependency to the :shared module, I’ve used gradlelibrary catalog for dependencies.
io.insert-koin:koin-core:3.5.0 - For Ktor, there are multiple dependencies each with their own purpose depending on your API.
- Since, Injection will be done at platform level so we need to expose the initialization part to both platforms. To do this we’ll create a Helper class with a method initKoin() and call this method from application classes for iOS and Android.
- initKoin() → We’ll define all our dependencies here which we’ll be needing later on.
// :shared/src/commonMain/kotlin/di/Koin.kt
fun initKoin() = startKoin {
modules(networkModule)
}
private val networkModule = module {
single {
HttpClient {
defaultRequest {
url.takeFrom(URLBuilder().takeFrom("https://internshala.com/"))
}
install(HttpTimeout) {
requestTimeoutMillis = 15_000
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
install(Logging) {
level = LogLevel.ALL
logger = object : Logger {
override fun log(message: String) {
println(message)
}
}
}
}
}
}
4. Calling this method from both platforms —
- iOS: We need to make changes at 2 different files. (shared iOS file and platform specific)
// :shared/src/iosMain/kotlin/Koin.ios.kt
class Koin {
fun initKoin() {
di.initKoin()
}
}
// iosApp/iosApp/iOSApp.swift
import SwiftUI
import shared
@main
struct iOSApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
init() {
KoinKt.doInitKoin()
}
}
- Android: Unlike iOS, for Android we can directly call the :shared initKoin() method and Koin will be initialized.
class App : Application() {
override fun onCreate() {
super.onCreate()
initKoin()
}
}
Mapping API response with the Result class —
We’ll be writing a small extension on the Ktor’s HttpClient for doing all th API calls and wrapping the response in Result (success / error) with proper message.
suspend inline fun <reified T> HttpClient.fetch(
block: HttpRequestBuilder.() -> Unit
): Result<T> = try {
val response = request(block)
if (response.status == HttpStatusCode.OK)
Result.Success(response.body())
else
Result.Error(Throwable("${response.status}: ${response.bodyAsText()}"))
} catch (e: Exception) {
Result.Error(e)
}
sealed interface Result<out R> {
class Success<out R>(val value: R) : Result<R>
data object Loading : Result<Nothing>
class Error(val throwable: Throwable) : Result<Nothing>
}
Setting up the Paging Library
- After implementing Koin and Ktor, coming to Paging library. We’ll be using open-source library from cashapp.
- Since Paging provides multiple methods to handle error and API response, we’ll be creating a composabe which will take care of this Behaviour.
@Composable
fun <T : Any> PagingListUI(
data: LazyPagingItems<T>,
content: @Composable (T) -> Unit
) {
LazyColumn(
modifier = Modifier
.fillMaxSize()
.background(Color.White),
horizontalAlignment = Alignment.CenterHorizontally,
) {
items(data.itemCount) { index ->
val item = data[index]
item?.let { content(it) }
Divider(
color = UiColor.background,
thickness = 10.dp,
modifier = Modifier.border(border = BorderStroke(0.5.dp, Color.LightGray))
)
}
data.loadState.apply {
when {
refresh is LoadStateNotLoading && data.itemCount < 1 -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
Text(
text = "No Items",
modifier = Modifier.align(Alignment.Center),
textAlign = TextAlign.Center
)
}
}
}
refresh is LoadStateLoading -> {
item {
Box(
modifier = Modifier.fillParentMaxSize(),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator(
color = UiColor.primary
)
}
}
}
append is LoadStateLoading -> {
item {
CircularProgressIndicator(
color = UiColor.primary,
modifier = Modifier.fillMaxWidth()
.padding(16.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
}
refresh is LoadStateError -> {
item {
ErrorView(
message = "No Internet Connection.",
onClickRetry = { data.retry() },
modifier = Modifier.fillParentMaxSize()
)
}
}
append is LoadStateError -> {
item {
ErrorItem(
message = "No Internet Connection",
onClickRetry = { data.retry() },
)
}
}
}
}
}
}
@Composable
private fun ErrorItem(
message: String,
modifier: Modifier = Modifier,
onClickRetry: () -> Unit
) {
Row(
modifier = modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = message,
maxLines = 1,
modifier = Modifier.weight(1f),
color = androidx.compose.ui.graphics.Color.Red
)
OutlinedButton(onClick = onClickRetry) {
Text(text = "Try again")
}
}
}
@Composable
private fun ErrorView(
message: String,
modifier: Modifier = Modifier,
onClickRetry: () -> Unit
) {
Column(
modifier = modifier.padding(16.dp).onPlaced { _ ->
},
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = message,
maxLines = 1,
modifier = Modifier.align(Alignment.CenterHorizontally),
color = androidx.compose.ui.graphics.Color.Red
)
OutlinedButton(
onClick = onClickRetry, modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
.wrapContentWidth(Alignment.CenterHorizontally)
) {
Text(text = "Try again")
}
}
}
Note: Skipping the PagingData implementation here which will be out of the scope for this article. If you want the full implementation please go through the GitHub repo attached at the end.
This is it. We’re done with the Paging.
Setting up the UI part for App & Internship Screen —

Entry Point for our App — App.kt
// :shared/src/commonMain/kotlin/App.kt
@Composable
fun App() {
MaterialTheme {
val screens = Screen.values()
var selectedScreen by remember { mutableStateOf(screens.first()) }
Scaffold(
bottomBar = {
BottomNavigation(
backgroundColor = Color.White,
modifier = Modifier.height(64.dp)
) {
screens.forEach { screen ->
BottomNavigationItem(
modifier = Modifier.background(Color.White),
selectedContentColor = ui.theme.Color.textOnPrimary,
unselectedContentColor = Color.Gray,
icon = {
Icon(
imageVector = getIconForScreen(screen),
contentDescription = screen.textValue
)
},
label = { Text(screen.textValue) },
selected = screen == selectedScreen,
onClick = { selectedScreen = screen },
)
}
}
},
content = { getScreen(selectedScreen) }
)
}
}
@Composable
fun getIconForScreen(screen: Screen): ImageVector {
return when (screen) {
Screen.INTERNSHIPS -> Icons.Default.AccountBox
Screen.JOBS -> Icons.Default.Add
Screen.COURSES -> Icons.Default.Notifications
else -> Icons.Default.Home
}
}
@Composable
fun getScreen(selectedScreen: Screen) = when (selectedScreen) {
Screen.INTERNSHIPS -> InternshipsScreen().content()
Screen.JOBS -> JobsScreen()
Screen.COURSES -> CoursesScreen()
else -> HomeScreen()
}
Internship Screen to Fetch paginated Data —
class InternshipsScreen : KoinComponent {
private val viewModel: InternshipViewModel by inject()
@Composable
fun content() {
val result by rememberUpdatedState(viewModel.internships.collectAsLazyPagingItems())
return Scaffold(
topBar = {
TopAppBar(
title = { Text("Internships") },
elevation = 0.dp,
navigationIcon = {
IconButton(onClick = { println("Drawer clicked") }) {
Icon(imageVector = Icons.Default.Menu, contentDescription = "Menu")
}
},
actions = {
IconButton(onClick = { println("Search Internships!") }) {
Icon(imageVector = Icons.Default.Search, contentDescription = "Search")
}
},
backgroundColor = Color.White
)
},
drawerContent = { /*Drawer content*/ },
content = { PagingListUI(data = result, content = { InternshipCard(it) }) },
)
}
}
Sneak Peak of the Output —

References:
Hope I made it a little simpler for you to learn things. If you have any doubts or feedback, please connect. And, Don’t forget to 👏.