Compose Multiplatform Networking using Ktor & Koin (Part 2)

Before starting Compose Multiplatform for networking , I would like to go through the my previous blog (part 1) in this blog we have covered about How Compose Multiplatform works with KMM support & how UI is being shared across different platforms. & also go through to how dependecy injection Koin works & in this demo we are creating a simple LazyverticalGrid from network call using Ktor client
Here is the link for part 1
link for How dependency injection koin works
What is Ktor?
Ktor is used for asynchronous client & server applications, Here we will use ktor client to interact with backend
Lets start with quick implementation
First step is to define versions in versions catalog file libs.versions.toml
ktor = "2.3.7"
koin="1.1.0"
ktor-client-core = { module = "io.ktor:ktor-client-core", version.ref = "ktor" }
ktor-client-okhttp = { module = "io.ktor:ktor-client-okhttp", version.ref = "ktor" }
ktor-client-darwin = { module = "io.ktor:ktor-client-darwin", version.ref = "ktor" }
ktor-client-cio= {module ="io.ktor:ktor-client-cio", version.ref = "ktor"}
ktor-client-android = { module = "io.ktor:ktor-client-android", version.ref = "ktor" }
kotlin-serialization = {module = "io.ktor:ktor-serialization-kotlinx-json", version.ref="ktor"}
media-kamel = {module="media.kamel:kamel-image", version.ref="kamel"}
koin-compose = {module="io.insert-koin:koin-compose", version.ref = "koin"}
ktor-client-content-negotiation = {module = "io.ktor:ktor-client-content-negotiation", version.ref= "ktor"}
[bundles]
ktor = ["ktor-client-core", "ktor-client-content-negotiation"]
Lets include it in build.gradle.kts
sourceSets {
val desktopMain by getting
androidMain.dependencies {
implementation(libs.compose.ui.tooling.preview)
implementation(libs.androidx.activity.compose)
implementation(libs.ktor.client.android)
}
commonMain.dependencies {
implementation(compose.runtime)
implementation(compose.foundation)
implementation(compose.material)
implementation(compose.ui)
@OptIn(ExperimentalComposeLibrary::class)
implementation(compose.components.resources)
implementation(libs.bundles.ktor)
implementation(libs.ktor.client.content.negotiation)
implementation(libs.kotlin.serialization)
implementation(libs.media.kamel)
implementation(libs.koin.compose)
}
desktopMain.dependencies {
implementation(compose.desktop.currentOs)
implementation(libs.ktor.client.cio)
}
iosMain.dependencies {
implementation(libs.ktor.client.darwin)
}
}
In the previous blog we have seen that how can we declare a different modules for koin
NetworkModule.kt
val providehttpClientModule = module {
single {
HttpClient {
install(ContentNegotiation) {
json(json = Json { ignoreUnknownKeys = true }, contentType = ContentType.Any)
}
}
}
}
here HttpClient is client for configuring asynchronous client
RepositoryModule.kt
val provideRepositoryModule = module {
single<NetworkRepository> { NetworkRepository(get()) }
}
ViewModelModule.kt
val provideviewModelModule = module {
single {
HomeViewModel(get())
}
}
Combine list of All modules we have
AppModule.kt
fun appModule() = listOf(providehttpClientModule, provideRepositoryModule, provideviewModelModule)
Start koin by inserting into Application level
Koin Application is used for start a new koin application in compose context
@Composable
fun App() {
KoinApplication(application = {
modules(appModule())
}) {
MaterialTheme {
Column(Modifier.fillMaxWidth(), horizontalAlignment = Alignment.CenterHorizontally) {
HomeScreen()
}
}
}
}
Kotlin serialization consists of a compiler plugin, that generates visitor code for serializable classes, runtime library with core serialization API and support libraries with various serialization formats.
- Supports Kotlin classes marked as
@Serializable
and standard collections.
Data Model Class
@Serializable
data class ApiResponse(
@SerialName("products")
var list: List<Products>
)
@Serializable
data class Products (
@SerialName("id")
var id: Int=0,
@SerialName("title")
var title: String="",
@SerialName("description")
val description: String="",
@SerialName("price")
val price: Double=0.0,
@SerialName("discountPercentage")
val discountPercentage: Double=0.0,
@SerialName("category")
val category: String="",
@SerialName("thumbnail")
val thumbnail: String="",
)
Repository class
class NetworkRepository(private val httpClient: HttpClient) {
fun getProductList(): Flow<NetWorkResult<ApiResponse?>> {
return toResultFlow {
val response = httpClient.get("api url").body<ApiResponse>()
NetWorkResult.Success(response)
}
}
}
ViewModel for UI state management
class HomeViewModel(private val networkRepository: NetworkRepository) {
private val _homeState = MutableStateFlow(HomeState())
private val _homeViewState: MutableStateFlow<HomeScreenState> = MutableStateFlow(HomeScreenState.Loading)
val homeViewState = _homeViewState.asStateFlow()
suspend fun getProducts() {
CoroutineScope(Dispatchers.IO).launch {
try {
networkRepository.getProductList().collect{response ->
when(response.status){
ApiStatus.LOADING->{
_homeState.update { it.copy(isLoading = true) }
}
ApiStatus.SUCCESS->{
_homeState.update { it.copy(isLoading = false, errorMessage = "", response.data) }
}
ApiStatus.ERROR->{
_homeState.update { it.copy(isLoading = false,errorMessage = response.message) }
}
}
_homeViewState.value = _homeState.value.toUiState()
}
} catch (e: Exception) {
_homeState.update { it.copy(isLoading = false,errorMessage ="Failed to fetch data") }
}
}
}
sealed class HomeScreenState {
data object Loading: HomeScreenState()
data class Error(val errorMessage: String):HomeScreenState()
data class Success(val responseData: ApiResponse):HomeScreenState()
}
private data class HomeState(
val isLoading:Boolean = false,
val errorMessage: String?=null,
val responseData: ApiResponse?=null
) {
fun toUiState(): HomeScreenState {
return if (isLoading) {
HomeScreenState.Loading
} else if(errorMessage?.isNotEmpty()==true) {
HomeScreenState.Error(errorMessage)
} else {
HomeScreenState.Success(responseData!!)
}
}
}
}
Main Compose class
@Composable
fun HomeScreen(){
val viewModel: HomeViewModel= getKoin().get()
val homeScreenState by viewModel.homeViewState.collectAsState()
LaunchedEffect(Unit) {
viewModel.getProducts()
}
when (homeScreenState) {
is HomeViewModel.HomeScreenState.Loading -> {
PiProgressIndicator()
}
is HomeViewModel.HomeScreenState.Success -> {
val products = (homeScreenState as HomeViewModel.HomeScreenState.Success).responseData.list
ProductCard(products)
}
is HomeViewModel.HomeScreenState.Error -> {
//show Error
}
}
}
Don’t forget to add Internet permission on Android Manifest file
androidMain (AndroidManifest.xml)
<uses-permission android:name="android.permission.INTERNET"/>
That's it!! See Full Implementation on My Github Repository!!

Please let me know your valuable inputs in the comments.
I would appreciate Claps if you find this article useful. Thank you !!