Compose Multiplatform 実践:CMPでKoinを使用した依存性注入(Dependency Injection)
はじめに
Compose Multiplatform (略称CMP)
こんにちは、皆さん
今日はCMPの応用について紹介を続けます
Koin
を使用して依存性注入を行い
コード間の結合度を下げ
より保守しやすくします
元々Androidを開発していた人はDagger2
やHilt
を使用していたかもしれません
しかしCMPが公式にサポートしているのは主にKoin
です
他のDIソリューション
を使用するには、別のワークアラウンドが必要かもしれません
そのため、今日はKoin
をCMPに導入することを中心に説明します
依存性注入(Dependency Injection)とは何か?
ソフトウェア開発において
高結合度(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>()
CMPでKoinを実装する
まず
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 可以大大簡化跨平台項目的依賴管理
- 根據項目規模和複雜度,選擇合適的初始化方式