Compose Multiplatform 実践:CMPでKoinを使用した依存性注入(Dependency Injection)
Compose Multiplatform (略称CMP)
こんにちは、皆さん
今日はCMPの応用について紹介を続けます
Koin
を使用して依存性注入を行い
コード間の結合度を下げ
より保守しやすくします
元々Androidを開発していた人はDagger2
やHilt
を使用していたかもしれません
しかしCMPが公式にサポートしているのは主にKoin
です
他のDIソリューション
を使用するには、別のワークアラウンドが必要かもしれません
そのため、今日はKoin
を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開發跨平台資料庫 & 疑難雜症
ソフトウェア開発において
高結合度(Coupling)
とは、コード内のモジュールやコンポーネント間に過度の依存関係が存在することを指します
これによりコードの保守やテストが難しくなります
高結合度の問題を解決するために
依存性注入(Dependency Injection, DI)
を使用してコード間の結合度を減らすことができます
依存性注入は一種の設計パターンで
オブジェクトのライフサイクル中に依存関係をオブジェクトに注入することを可能にします
オブジェクト内部でインスタンスを作成するのではなく
これによりコードがより柔軟でテスト可能になります
先日StateFlowを構築する際に使用したViewModelの例を見てみましょう
ここではSettingViewModelインスタンス
を手動で作成し、内部に複数のクラスを初期化する必要があります
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()
...
...
...
}
}
DIを使用してViewModelを導入する場合
自分でインスタンスを作成する必要はありません
fun NavGraphBuilder.routeSettingScreen(
navController: NavHostController,
) {
composable(ElegantJapaneseScreen.Setting.name) {
val viewModel = koinViewModel<SettingViewModel>()
SettingScreen(navController, viewModel)
}
}
インスタンス作成がkoinViewModelを通じて注入されるだけに見えるかもしれません
しかし、詳しく見ると
もしSettingViewModel
のコンストラクタが突然複雑になった場合
必要なインスタンスをそれぞれ作成する必要が出てきます
例えば:
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,...)
ここでDI注入の利点が明らかになります
自分でひとつずつインスタンスを作成するステップを省略でき
コードの行数が少なく、より簡潔になります
もう一つの利点は
コードを変更する必要がある場合
柔軟性が高まることです
注入モジュールのコードだけを変更すればよく
元のViewModel
のコードは変更せずに
モジュールの実装部分のロジックだけを修正します
さらに
複数の場所で特定のクラスを使用する必要がある場合
依存性注入は自分でインスタンスを作成するステップを減らすことができます
class SettingViewModel(private val a: A, private val b: B,...) {
...
}
使用時は引き続き
val viewModel = koinViewModel<SettingViewModel>()
まず
lib.versions.toml
ファイルにkoin
の依存関係とバージョン番号を追加します
完了したら
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" }
- 今回も共有するので、
commonMain
に以下を追加します:
sourceSets {
commonMain.dependencies {
implementation(libs.koin.core)
implementation(libs.koin.compose.viewmodel)
implementation(libs.koin.compose)
}
}
(前回の各プラットフォームのエントリポイントを覚えていますか?
忘れた場合は戻って見てください)
- まず
commonMain
にplatformModule
のexpect
を追加します
ターゲットプラットフォームによって実装方法が異なる可能性があるため
CMPがまだサポートしていない場合
platformModule
を通じて個別に実装および注入することができます
これにより異なるターゲットプラットフォームのコンテンツをcommonMain
に注入できます
例:永続ストレージdataStore、ローカライズストレージRoomDatabaseのBuilder…など
ここではcommonMain
でplatformModule
のexpect
を定義し、
appModule()
は主により多くのモジュールを配置または拡張
するために使用します
// in ../commonMain
expect val platformModule: Module
fun appModule() =
listOf(platformModule,...)
次にandroidMain
とiosMain
のplatformModuleを実装します
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() }
}
- 次に
CMPターゲットプラットフォーム
でKoinを設定する必要があります
まずandroidMain
にKoinを追加します
// 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()
}
}
}
關鍵程式碼解說
:
androidModule
:因為不管Android
平台或是iOS平台
可能會有他們自定義的規則
例如:Android有Context
,但是iOS沒有
可以先做一個androidModule
把Context
實例配置進koin裡面startKoin
:接著我們就可以把androidModule跟前面做的appModule()
帶進來
而根據前面的程式碼
我們前面autual 的platformModule
也會被帶進來
- 開始配置
iosMain
的koin
// 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
}
關鍵程式碼解說
:
-
iosModule
:跟android
一樣,ios也有獨有的東西UIViewController
,若剛好需要
則可以把它做成一個iosModule
-
KoinApplication
:接著我們就可以把iosModule
跟前面做的appModule()
帶進來
而根據前面的程式碼
我們前面autual 的platformModule
也會被帶進來
前個區塊主要介紹了在多個目標平台上進行開發的方法
現在,我們終於可以開始開發共用模組
在 koin
中製作模組也相對直觀
首先,我們來看一下如何在 commonMain
中定義 koin
的模組
// 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()) }
}
關鍵程式碼解說
:
single { A() }
: 定義一個單例 A 的實例
每次注入 A 時,koin 都會返回同一個實例
single { B() }
和single { C() }
: 同樣定義單例 B 和 C 的實例single { SettingViewModel(get(), get(), get()) }
: 定義SettingViewModel
的單例實例,並通過get()
方法從koin
容器中注入 A, B, C 的實例。appModule()
:前面我們就有先定義他了,現在把新的module加入即可。- 這個module主要使用koin library內提供的
module{}
去創建
核心概念
就是把你想要的instance給創建進來
而當配置好startKoin
時
你就可以透過koin
幫你inject
進來
現在我們能開心解放
複雜的手動創instance了
fun NavGraphBuilder.routeSettingScreen(
navController: NavHostController,
) {
composable(ElegantJapaneseScreen.Setting.name) {
val viewModel = koinViewModel<SettingViewModel>()
SettingScreen(navController, viewModel)
}
}
- Koin 能在 Compose Multiplatform 使用
- 通過適當的配置,可以在不同平台上靈活使用 Koin
- 使用 Koin 可以大大簡化跨平台項目的依賴管理
- 根據項目規模和複雜度,選擇合適的初始化方式