ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Follow publication

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

Published in ProAndroidDev

The latest posts from Android Professionals and Google Developer Experts.

Written by Nimit Raja

Android, Java , Kotlin, Compose, KMP, Dart, Community Member @Kotzilla ,Tech Writer, Open Source Contributor

Responses (1)

Write a response