Compose Multiplatform Networking using Ktor & Koin (Part 2)

Nimit Raja
ProAndroidDev
Published in
4 min readJan 6, 2024

--

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 !!

--

--

Sr Android Engineer, Jetpack Compose, KMP, Dart, Tech Writer, Open Source Contributor