Compose Multiplatform in Action: Using Koin for Dependency Injection in CMP
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
目錄
- Compose Multiplatform 實戰:放輕鬆點,初探CMP
- Compose Multiplatform 實戰:初戰,安裝CMP環境吧
- Compose Multiplatform 實戰:續戰,用Wizard創建CMP專案
- Compose Multiplatform 實戰:在Android、iOS模擬器上跑CMP專案
- Compose Multiplatform 實戰:CMP的專案結構理解與編譯配置
- Compose Multiplatform 實戰:CMP中跨平台Android、iOS程式碼的進入點
- Compose Multiplatform 實戰:在CMP的Compose中用Material Design3 Theme
- Compose Multiplatform 實戰:CMP用Compose實作跨平台畫面
- Compose Multiplatform 實戰:使用 expect 和 actual 實現跨平台程式碼
- Compose Multiplatform 實戰:CMP中實作Compose Navigation頁面切換
- Compose Multiplatform 實戰:CMP中透過StateFlow來管理UI狀態
- Compose Multiplatform 實戰:CMP中實作NavigationBar底部欄
- Compose Multiplatform 實戰:CMP中使用koin來依賴注入Dependency Injection
- Compose Multiplatform 實戰:CMP實作跨平台資料庫SqlDelight
- Compose Multiplatform 實戰:CMP中使用ROOM開發跨平台資料庫 & 疑難雜症
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>()
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 useplatformModule
to implement and inject separately
allowing different target platform content to be injected intocommonMain
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 toandroidMain
// 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 rules
For example:Android has Context
but iOS doesn’t
We can create anandroidModule
to configure aContext
instance in KoinstartKoin
: Then we can bring in the androidModule and the previously createdappModule()
Based onthe previous code
Our previously declaredplatformModule
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 aniosModule
for it -
KoinApplication
: Then we can bring in theiosModule
and our previously createdappModule()
Based onthe previous code
Our previously declaredplatformModule
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 A
Whenever A is injected, Koin will return the same instance
single { B() }
andsingle { C() }
: Similarly defines singleton instances of B and Csingle { 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 library
Thecore concept
is to create the instances you want
OncestartKoin
is configured
You can have theminjected
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)
}
}
- 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