Compose Multiplatform in Action: Using Koin for Dependency Injection in CMP
Introduction
Compose Multiplatform (CMP)
Hi everyone,
Today we'll continue exploring CMP applications
by using Koin
for dependency injection
to reduce coupling between code components
making them easier to maintain
Android developers might be familiar with Dagger2
or Hilt
but currently, CMP officially supports mainly Koin
Using other DI solutions
might require your own workarounds
so today we'll focus on integrating Koin
into CMP
What is Dependency Injection?
In software development,
high coupling
refers to excessive dependencies between modules or components in code
making code difficult to maintain and test
To solve high coupling issues,
we can use Dependency Injection (DI)
to reduce coupling between code components
Dependency Injection is a design pattern
that allows us to inject dependencies into objects during their lifecycle
rather than creating instances inside the objects
making code more flexible and testable
Let's look at an example from our StateFlow ViewModel from a few days ago
Here we need to manually create the SettingViewModel instance
and initialize multiple classes inside
fun NavGraphBuilder.routeSettingScreen(
navController: NavHostController,
) {
composable(ElegantJapaneseScreen.Setting.name) {
val viewModel = SettingViewModel()
SettingScreen(navController, viewModel)
}
}
class SettingViewModel() {
lateinit var a :A
lateinit var b :B
lateinit var c :C
lateinit var e :E
lateinit var f :F
init{
a = A()
b = B()
...
...
...
}
}
With DI, you don't need to create instances yourself
fun NavGraphBuilder.routeSettingScreen(
navController: NavHostController,
) {
composable(ElegantJapaneseScreen.Setting.name) {
val viewModel = koinViewModel<SettingViewModel>()
SettingScreen(navController, viewModel)
}
}
At first glance, it seems like we're just replacing instance creation with koinViewModel injection
But looking closer,
if your SettingViewModel
constructor becomes complex,
you'd need to create each required instance one by one
For example:
val a = A()
val b = B()
val c = C()
val d = D()
val e = E()
val f = F()
...
SettingViewModel(a,b,c,d,e,f,...)
This is where DI's advantages become clear
It eliminates the need to create instances manually
making your code shorter and cleaner
Another advantage is
when you need to modify your code,
it becomes more flexible
You just need to change the injected module code
The original ViewModel
code can remain unchanged
only modifying the logic in the module implementation
Additionally,
if you need to use a class in multiple places,
dependency injection reduces the steps to create instances.
class SettingViewModel(private val a: A, private val b: B,...) {
...
}
When using it, it's still simply:
val viewModel = koinViewModel<SettingViewModel>()
Implementing Koin in CMP
First,
we need to add Koin
dependencies and version numbers to the lib.versions.toml
file
After completion,
remember to sync gradle
[versions]
koin = "3.5.0"
koinCompose = "1.2.0-Beta4"
[libraries]
koin-core = { module = "io.insert-koin:koin-core", version.ref = "koin" }
koin-compose-viewmodel= { module = "io.insert-koin:koin-compose-viewmodel", version.ref = "koinCompose" }
koin-compose = { module = "io.insert-koin:koin-compose", version.ref = "koinCompose" }
- As usual, these are shared, so add the following to
commonMain
:
sourceSets {
commonMain.dependencies {
implementation(libs.koin.core)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.compose)
}
}
(Remember our discussion on entry points for different platforms?
If you've forgotten, go back and review it)
-
First, let's add an expect
platformModule
incommonMain
Since target platforms might have different implementations
If CMP doesn't support something yet
We can use
platformModule
to implement and inject separatelyallowing different target platform content to be injected into
commonMain
For example: persistent storage dataStore, localized storage RoomDatabase Builder, etc.
First, in commonMain
, declare an expect
for platformModule
&
appModule()
which helps us place or extend more modules
// in ../commonMain
expect val platformModule: Module
fun appModule() =
listOf(platformModule,...)
Then implement platformModule
for androidMain
& iosMain
androidMain
:
// in ../androidMain
actual val platformModule: Module = module {
/** Add some target class that you would like to get it instance*/
// for example : single { dataStore(get<Context>()) }
}
iosMain
:
// in ../iosMain
actual val platformModule: Module = module {
/** Add some target class that you would like to get it instance*/
// for example : single { dataStore() }
}
-
Now we need to configure Koin in
CMP target platforms
First, let's add Koin to
androidMain
// in ../androidMain/../MainActivity.kt
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val androidModule = module {
single<Context> { this@MainActivity.applicationContext }
}
startKoin {
modules(appModule() + androidModule)
}
setContent {
App()
}
}
}
Key code explanation
:
-
androidModule
: Because bothAndroid
andiOS platforms
might have their own rulesFor example:
Android has Context
but iOS doesn'tWe can create an
androidModule
to configure aContext
instance in Koin -
startKoin
: Then we can bring in the androidModule and the previously createdappModule()
Based on
the previous code
Our previously declared
platformModule
will also be included
- Now let's configure Koin for
iosMain
// in ../iosMain/../MainViewController.kt
fun MainViewController() = ComposeUIViewController {
val uiViewController = LocalUIViewController.current
val iosModule = module {
single<UIViewControllerWrapper> { UIViewControllerWrapperImpl(uiViewController) }
}
KoinApplication(application = {
modules(appModule() + iosModule)
}) {
App()
}
}
interface UIViewControllerWrapper {
fun getViewController(): UIViewController
}
class UIViewControllerWrapperImpl(private val viewController: UIViewController) : UIViewControllerWrapper {
override fun getViewController() = viewController
}
Key code explanation
:
-
iosModule
: Similar toandroid
, iOS also has unique elements likeUIViewController
If needed, we can create an
iosModule
for it -
KoinApplication
: Then we can bring in theiosModule
and our previously createdappModule()
Based on
the previous code
Our previously declared
platformModule
will also be included
The previous section mainly covered development methods across multiple target platforms
Now, we can finally start developing shared modules
Creating modules in Koin
is also relatively intuitive
First, let's see how to define Koin
modules in commonMain
// in ../commonMain
expect val platformModule: Module
fun appModule() =
listOf(platformModule, utilModule, viewModelModule ...)
val utilModule = module {
single { A() }
single { B() }
single { C() }
}
val viewModelModule = module {
single { SettingViewModel(get(), get(), get()) }
}
Key code explanation
:
-
single { A() }
: Defines a singleton instance of AWhenever A is injected, Koin will return the same instance
single { B() }
andsingle { C() }
: Similarly defines singleton instances of B and C -
single { SettingViewModel(get(), get(), get()) }
: Defines a singleton instance ofSettingViewModel
, and injects instances of A, B, C from theKoin
container using theget()
method. -
appModule()
: We defined this earlier, now we just add the new modules. -
This module primarily uses the
module{}
provided in the Koin libraryThe
core concept
is to create the instances you wantOnce
startKoin
is configuredYou can have them
injected
for youthrough Koin
Now we can happily free ourselves
from complex manual instance creation
fun NavGraphBuilder.routeSettingScreen(
navController: NavHostController,
) {
composable(ElegantJapaneseScreen.Setting.name) {
val viewModel = koinViewModel<SettingViewModel>()
SettingScreen(navController, viewModel)
}
}
Summary
- Koin can be used in Compose Multiplatform
- With proper configuration, Koin can be used flexibly on different platforms
- Using Koin can greatly simplify dependency management in cross-platform projects
- Choose the appropriate initialization method based on project size and complexity