Koin dependency injection for Compose Multiplatform with MVVM Pattern
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.

- 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