Koin dependency injection for Compose Multiplatform with MVVM Pattern

Andrea
4 min readSep 20, 2024

What is dependency injection ?
Dependency injection is a technique used in object-oriented programming (OOP) in which an object or function receives other objects or functions that it requires, as opposed to creating them internally to reduce the hardcoded dependencies between objects. In a simple word, we don’t create a dependency object but we inject it.

Why we need dependency injection?
A basic benefit of dependency injection is decreased coupling between classes and their dependencies. The most basic reason is to reduce hardcoded boilerplate dependencies between object. Other reason is we can maintain only one singleton object throughout the application.

One of the library I am using for Compose Multiplatform is KOIN. On my application will only cover Android platform & Web platform using WASM.
The application will need to send data using API/Http Client through Repository and it will be implemented from ViewModel.

  1. Setting KOIN dependency libraries
    Inside gradle -> libs.version.toml :
[version]
koin-android = "4.0.0"
koin-compose-multiplatform = "4.0.0"
koin = "4.0.0"

[libraries]
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin-android" }
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin" }
koin-compose-multiplatform = { module = "io.insert-koin:koin-compose", version.ref = "koin-compose-multiplatform" }

inside shared -> build.gradle.kts :

sourceSets {
val commonMain by getting {
dependencies {
//KOIN
api(libs.koin.core)
implementation(libs.koin.test)
implementation(libs.koin.compose.multiplatform)
//...
}
}

val androidMain by getting {
dependsOn(mobileMain)
dependencies {
//KOIN
implementation(libs.koin.androidx.compose)
//...
}
}
//...

2. Create all related class

object ApiServiceImpl {

suspend fun HttpClient.signIn(userName: String, password: String) =
fetch<SignInAuthResponse> {
url("login/signin")
method = HttpMethod.Post
contentType(ContentType.Application.Json)
setBody(Json.encodeToString(UserAccount(userName, password)))
}
}
interface AuthorizationRepository {
suspend fun signIn(userName: String, password:String): Flow<DataState<SignInAuthResponse>>
}

class AuthorizationRepositoryImpl(private val apiService: HttpClient): AuthorizationRepository {
override suspend fun signIn(userName: String, password:String): Flow<DataState<SignInAuthResponse>> {
return apiService.signIn(userName, password)
}
}
class AuthorizationViewModel(
private val accountRepository: AuthorizationRepository
) {
private val coroutineHandlerException =
CoroutineExceptionHandler { coroutineContext, throwable ->
println("error is ${throwable.message}")
}
private val viewModelScope =
CoroutineScope(Dispatchers.Unconfined + SupervisorJob() + coroutineHandlerException)

private val _signInState: MutableStateFlow<DataState<SignInAuthResponse>> = MutableStateFlow(DataState.InitState)
val signInUiState: StateFlow<DataState<SignInAuthResponse>> = _signInState

private fun signIn(userName: String, password: String) {
viewModelScope.launch {
try {
accountRepository.signIn(userName, password)
.collect{
_signInState.value = it
}
} catch (e: Exception) {
_signInState.emit(DataState.Error(Throwable(message = e.message)))
print(e.message)
}
}
}
// ....

As above code, we can see that ViewModel depends on the AuthorizationRepository, while AuthorizationRepository depends on HttpClient object.

3. Create application module
Use the module function to declare a Koin module. A Koin module is the place where we define all our components to be injected.

Declare our first component. We want a singleton of HttpClient, by creating an instance of it. Create file Koin.kt inside commonmain package :

val baseUrl = "https://opinino-be-1fbfb7a8d6c2.herokuapp.com/"
val networkModule = module {
single {
HttpClient {
defaultRequest {
url.takeFrom(URLBuilder().takeFrom(baseUrl))
}
install(HttpTimeout) {
requestTimeoutMillis = 5_000
}
install(ContentNegotiation) {
json(Json {
ignoreUnknownKeys = true
prettyPrint = true
})
}
}
}
}

In a same way we create singleton of AuthorizationRepository, by creating an instance of AuthorizationRepositoryImpl , using get() function to get HttpClient object.

val appModules = module {
single<AuthorizationRepository> { AuthorizationRepositoryImpl(get()) }
}

Next we will create the ViewModel. We declare ViewModel in our Koin module. We declare it as a factory definition, to not keep any instance in memory and let the native system hold it:

val appModules = module {
single<AuthorizationRepository> { AuthorizationRepositoryImpl(get()) }
factory { AuthorizationViewModel(get()) }
}

And the last, we will create StartKoin() function inside the same file to kickstart the entire process.

fun initKoin() = startKoin {
modules(
appModules,
networkModule,
getPlatformModule()
)
}

4. Inject ViewModel to Android application. To get it into our Activity, let’s inject it with the by inject() delegate function:

class MainActivity : ComponentActivity() {
private val viewModel : AuthorizationViewModel by inject()

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
MaterialTheme {
Surface(
modifier = Modifier.fillMaxSize()
) {
App(viewModel)
}
}
}
}
}

And call the StartKoin() function from Application

class ExampleApp : Application() {
override fun onCreate()
super.onCreate()
initKoin()

}
}

5. Inject ViewModel to Web application. For Web application, I found that I can’t used by inject() but instead I need to called get() function.

@OptIn(ExperimentalComposeUiApi::class)
fun main() {
val koinApp = initKoin().koin
val viewModel = koinApp.get<AuthorizationViewModel>()

CanvasBasedWindow(canvasElementId = "ComposeTarget") {
App(viewModel)
}
}

When needed, it is also possible to inject the ViewModel object directly into the class using koinInject()

@Composable
fun HomeNav(navigator: NavHostController,
showBottomSheet: () -> Unit)
{
NavHost (
startDestination = OpininoHomeScreen.Home.name,
navController = navigator
) {
composable(route = OpininoHomeScreen.Home.name) {
val viewModel: PollViewModel = koinInject()
...
...
}

And that’s the wrap. My compose multiplatform application is all set.

More reading on this topics
- Koin tutorial for KMP
- The release KOIN support for WASM

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

Andrea
Andrea

No responses yet

Write a response