[Android]KoinでDIに入門する

2020.05.13

はじめに

CX事業本部のほりです。

今までDIを導入する機会がなく、個人でDaggerに手を出してみるも「何も分からない」状態のまま挫折しておりました。 「いつか勉強しようリスト」に入っていたDIですが、今回Koinというライブラリで勉強する機会を得たため、その概要をまとめてみます。

環境

Android Studio:3.6
koin_version:2.1.5

Koinの導入

公式サイトはこちら 今回はMainRepositoryインスタンスを持ったMainViewModelをDIしてみます。 (MainViewModelがMainRepositoryに依存している状態)

class MainViewModel(private val repository: MainRepository) :ViewModel(){

    fun call(){
        repository.hello()
    }
}
class MainRepository {

    fun hello() {
        Log.d(this::class.java.simpleName, "Hello,World")
    }

    companion object {
        private var instance: MainRepository? = null

        fun getInstance(): MainRepository =
            instance ?: synchronized(this) {
                instance ?: MainRepository().also { instance = it }
            }
    }

}

KoinでのViewModelのDIは
① モジュールを宣言
② ApplicationクラスでKoinを開始

class App : Application() {

    override fun onCreate() {
        super.onCreate()
        //②
        startKoin {
            modules(listOf(module))
        }
    }
    //①
    private val module: Module = module {
        viewModel { MainViewModel(get()) }
        single { MainRepository() }
    }

}

③対象のActivity/Fragmentで遅延初期化

class MainActivity : AppCompatActivity() {
    //③
    private val viewModel by viewModel<MainViewModel>()
    ~~~~~~~~~
}

の3ステップだけで完了します。すごい。
しかしこの状態だと何が行われているか分からないため、内部をみてみると共に自分でも同じような機能を作成してみます。

ViewModelのDI部分を置き換えてみる

Koinが内部で何をやっているかを調べると (KoinのDIの仕組みを参考にさせていただきました) startKoin()Koin内にModuleで宣言したオブジェクトを*保存し、 委譲プロパティでそれを取得する、ということが追えます。

*Moduleにはシングルトンを生成するsingle()というメソッドと、注入するごとにオブジェクトを生成するfactory()というメソッドが存在しており、viewModel()factory()を内部で呼び出しています。

自作のシングルトンオブジェクトに置き換えるとこのように書けます。

object InjectionUtil {

    fun provideMainViewModelFactory(): MainViewModelFactory {
        val repository = getMainRepository()
        return MainViewModelFactory(repository)
    }

    private fun getMainRepository(): MainRepository {
        return MainRepository.getInstance()
    }
}
@Suppress("UNCHECKED_CAST")
class MainViewModelFactory(
    private val repository: MainRepository
) : ViewModelProvider.Factory {

    override fun <T : ViewModel?> create(modelClass: Class<T>): T = MainViewModel(repository) as T
}
class MainActivity : AppCompatActivity() {

    private val viewModel by viewModels<MainViewModel> {
        InjectionUtil.provideMainViewModelFactory()
    ~~~~~~~~~
}

Koinを用いた場合と同様に外部からViewModelのインスタンスを取得する事ができる様になりました。 (実際はKoinだとby viewModel() という専用の拡張関数を使いgetKoin().getViewModel()内で対応する型のインスタンスを取得しています)

ここで書き換えた内容をKoinは内部でやってくれているんですね。感謝。

感想

AACのViewModelのコンストラクタに引数を渡そうとする際は、 ViewModelProvider.Factoryインターフェースを実装したクラスを自作し、インスタンスを作成するというお作法のような書き方があります。 Koinを使用するとget()内で対応する型のインスタンスを取得した上でViewModelの作成まで内部で処理してくれる為、非常に楽です。

より効果的にDIを使用する為には、Repositoryのインターフェースを作成し ViewModelをインターフェースに依存させることで、モックアップとの差し替えを可能にできます。 ただ「DIでテストが書きやすくなる」という雰囲気だけの知識を持っていた私としては、 テストを書かないとしてもアプリの拡張性が高まる為導入するメリットが大きいと感じました。 ViewModel用のFactoryクラスを自作する手間が省けるだけでも使用する価値があるのではないかと思います。

まとめ

  • DI = 依存オブジェクトを外部から注入すること
  • Koin = DIを楽に導入するためのDIコンテナ
  • Koinはシンプルで簡単に使える

今回試したコードはこちらに置いてあります。 以上です。ありがとうございました。