🪙

【Compose Multiplatform】依存性注入を対応させる(Hilt -> Koin)

2023/09/10に公開

はじめに

ナビゲーションに続き、依存性注入(DI)をCompose Multiplatformに対応させました。
依存性注入は、AndroidプロジェクトではHiltを使うことがデファクトスタンダードになっていますが、こちらはCompose Multiplatformに対応しておりません。
Compose Multiplatform Wizardによると、Koinというライブラリが推奨されていたので、こちらに移行していきたいと思います。
https://terrakok.github.io/Compose-Multiplatform-Wizard/

こちらの記事では、それぞれの詳細な説明というよりも、HiltKoinとの対応関係を明確化させることを中心に書いていきたいと思います。(依存性注入の知識が、自分の中でそこまでかっちりとしていないのもあります...)

Koinの公式サイト

https://insert-koin.io/docs/setup/koin

HiltとKoinの違い

Hilt Koin
使用開始 Application継承クラスで@HiltAndroidApp Application継承クラスでstartKoin{modules(appModule)}
モジュール寿命 Singletonm, ViewModel, Activity, Fragment, View...など8種類(公式) single, facrory, scopeの3種類(公式)
モジュールの定義 @Provides fun provideHoge という関数をもつ、@ModuleをつけたHogeModuleを定義 val hogeModule = module{}というグローバル変数として定義
注入の仕方 クラスのコンストラクタかクラス内に、@Injectをつけた変数(または関数)として定義 KoinComponent継承クラスで val hoge: Hoge by inject() で定義
アノテーション たくさん使う 使わない
理解の難易度 暗黙の約束が多く、最初はなんで動くのか意味不明 明快

Koinの導入

下記のように依存関係を追加します。執筆時の最新バージョンは3.4.3でした。

app/build.gradle.kts
implementation "io.insert-koin:koin-android:$koin_android_version"

Koinの使用開始

Hiltをすでに使っている場合は、Applicationクラスを継承したクラスを@HiltAndroidAppアノテーションをつけて定義しているので、それをそのまま使います。

MainApplication
@HiltAndroidApp
class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()
	
	// 各種処理
    }
}

そのクラスからHilt用のアノテーションを削除して、開始処理を追加します。

MainApplication
- @HiltAndroidApp
class MainApplication: Application() {
    override fun onCreate() {
        super.onCreate()
	
+	startKoin{
+	    modules(appModule) // appModuleは、のちに定義するモジュール群
+	}
	
	// 各種処理
    }
}

注入のための三人衆の定義

interface, impl(implementation), moduleの三人衆を定義します。(勝手にそう呼んでます)

Hiltでの三人衆

interfaceの定義

HogeRepository.kt
interface hogeRepository {
    fun getHoge(): Hoge
}

implの定義

HogeRepositoryImpl.kt
class HogeRepositoryImpl: HogeRepository {
    override fun getHoge(): Hoge {
        return // 具体的な処理
}

moduleの定義

HogeRepositoryModule.kt
@Module
@InstallIn(SingletonComponent::class)
class HogeRepositoryModule() {
    @Provides
    @Singleton
    fun provideHoge(hogeImpl: HogeImpl): Hoge {
        return hogeImpl
    }
}

Koinでの三人衆

interfaceの定義(Hiltと同じ)

HogeRepository.kt
interface hogeRepository {
    fun getHoge(): Hoge
}

implの定義(ここで何も注入したりしない場合、Hiltと同じ)

HogeRepositoryImpl.kt
class HogeRepositoryImpl: HogeRepository {
    override fun getHoge(): Hoge {
        return // 具体的な処理
}

moduleの定義

HogeRepositoryModule.kt
val hogeRepositoryModule = module {
    single<HogeRepository> { HogeRepositoryImpl() }
}

注入してみる

Hiltで注入する(3種類)

Hiltでの注入は以下のように行いました。(一般的な場合: コンストラクタインジェクション)

UsingHogeViewModel.kt
class UsingHogeViewModel @Inject(hogeRepository: HogeRepository): ViewModel(またはScreenModel) {
     val hoge = hogeRepository.getHoge()
     
     // 各種処理
}

またはこういう書き方もありなようです。(ActivityやFragmentなど、コンストラクタをいじれない場合: フィールドインジェクション (参考))

UsingHogeViewModel.kt
class UsingHogeViewModel: ViewModel(またはScreenModel) {
    @Inject
    lateinit var hogeRepository: HogeRepository
    
    val hoge: Hoge
        get() = hogeRepository.getHoge()
     
     // 各種処理
}

ここで、val hoge = hogeRepository.getHoge()として使うことはできず、getメソッドを使う必要がある点に注意です。(参考

もしくは、こういう書き方もアリなようです。(あるメソッド内でしか使わない場合など: メソッドインジェクション)

UsingHogeViewModel.kt
class UsingHogeViewModel: ViewModel(またはScreenModel) {
    @Inject
    fun getHoge(hogeRepository: HogeRepository): Hoge {
        return hogeRepository.getHoge()
    }

    // 各種処理
}

Koinで注入する

Koinでは以下のようになります。

UsingHogeViewModel.kt
- class UsingHogeViewModel @Inject(hogeRepository: HogeRepository): ViewModel(またはScreenModel) {
+ class UsingHogeViewModel: ViewModel(またはScreenModel), KoinComponent {
+    val hogeRepository: HogeRepository by inject()
     val hoge = hogeRepository.getHoge()
     
     // 各種処理
}

おわりに

Koinの方が暗黙の了解が少ないため、個人的には非常に好みです。実装もシンプルになってマルチプラットフォーム対応もできるとは一石二鳥ではないですか。もちろんHiltの方が、モジュールの寿命をもっと高解像度に定義したい場合などは優れていると思いますし、Googleに公式でサポートされているのは強いですね。

まだまだ依存性注入について深く語れるほど知識がないので、やや雑な記事になってしまいましたが、勉強を重ねていきたいと思います。

Discussion