虚苦心観察ブログ

ブログ管理者である虚苦心が私利私欲に基づいて書いているブログです。主にガジェットのレビューだったり、画像処理のことだったりを記事にしています。

Koin でDI

発端

DIについて勉強したいと思いDIフレームワークを探していたところKoinというDIフレームワークを見つけました。 しかし最近メジャーバージョンが上がったためか情報が少なかったのでメモとしてまとめます。

Koinとは

insert-koin.io
KoinとはKotlinで書かれたDIフレームワークです。
拡張関数やbyによるDelegateを多用しています。

なのでKotlinに慣れてないと所見では理解しにくいのではないかと思います。僕は理解できなかったですが、とりあえず書くことで理解できました。

拡張関数とか使っているのでKotlinじゃないと使えないのか?と思っていたのですがJavaからも呼び出せるようです。バイトコードが同じだからそれはそうですよね。

またKoinを使おうと思った理由はAnnotationを使わないということです。
Annotationは便利なのですが、エラーになった時にエラーを追いにくいのが苦手です。

Koinのバージョンについて

2018年9月にメジャーバージョンが1になったそうです medium.com またフレームワークAPIが大幅に変わっているようで、ちょっと情報がすくない状態です。 さらに2.0のbetaが公開されており、まだまだAPIが変わる可能性があり、長期的に利用するにはちょっと辛いかもしれません。(Androidのほうがつらいので気にならない気もします)

AndroidでのKoinの使い方

基本的な使い方

interface MyContract {
  interface Presenter {}
  interface View{}
}

class MyPresenter: MyContract.Presenter {}

class MyFragment: MyContract.View {
  val presenter: Presenter by inject()
}

class MyApplication : Application() {
  override fun onCreate(){
    super.onCreate()
    // start Koin!
    startKoin(this, listOf(myModule))
  } 

  val myModule = module {
    factory<MyContract.Presenter>{ MyPresenter() }
  }
} 

ざっとですが、KoinでのDIはこんな感じでできます。 Koinの初期化はApplicationクラスで行います。
startKoinメソッドでDIに使うクラスの初期化を行っています。
startKoinメソッドに渡す変数はKoinのModuleクラスのListです。Listに与えているModuleのインスタンスはApplicationクラスの拡張関数で生成しています。
Moduleインスタンス内ではfactory、singleメソッドを使うことができ、それぞれFactoryメソッドとして、Singletonを生成するメソッドとして動作します。
factory、singleメソッドの戻り値は型推定によって自動的に設定されるか、明示的に設定することが可能です。DI時には型かfactory、singleに指定可能なnameを頼りにインスタンスが生成されます。なのでDIする対象がinterfaceの場合はfactory、singleの戻り値もinterfaceにしておかないとクラスが見つけられずにエラーになってしまいます。

以上の理由から今回の例ではMyViewクラスのpresenterの型がMyContract.Presenterなのでfactory<MyContract.Presenter>としてfactoryの戻り値を明示的に指定しています。

コンストラクタに引数がある場合

先ほどの例ではDIするクラスのコンストラクタには引数がありませんでしたが、コンストラクタに引数がある場合について示します。

interface MyContract {
  interface Presenter {}
  interface View{}
  interface Repository {}
}

class MyPresenter(val view: View, val repository: Repository): MyContract.Presenter {}

class MyFragment: MyContract.View {
  val presenter: Presenter by inject { parameterOf(this) }
}

class MyRepository: MyContract.Repository {}

class MyApplication : Application() {
  override fun onCreate(){
    super.onCreate()
    // start Koin!
    startKoin(this, listOf(myModule))
  } 

  val myModule = module {
    factory<MyContract.Presenter>{(view: MyContract.View) -> MyPresenter(view, get()) }
    single<MyContract.Repository>{ MyRepository() }
  }
} 

この例ではMyPresenterのコンストラクタにviewとrepositoryの引数を与えています。
このような引数付きのコンストラクタではfactory<MyContract.Presenter>{(view: MyContract.View) -> MyPresenter(view, get()) }のようにしてインスタンスを生成します。viewはinjectメソッドの引数から与えられるようにし、repositoryはSingletonで生成するのでkoinのget()メソッドによって与えられます。 MyFragmentでのpresenterへのDIはinjectメソッドを介してval presenter: Presenter by inject(this)のようにして引数にviewであるthisを与えています。

ActivityやFragment以外でDIしたい場合

Koinは拡張関数を使ってDIしています。なので既知のクラスを使っている必要があります。
AndroidではActivityとFragmentが既知のクラスにあたり、これらに拡張関数が設定されているため何も意識せずにDIすることができます。
しかし、ActivityやFragment以外でDIしたい場合もあります。 このような場合はKoinComponentインターフェースを使用します。
KoinComponentはメソッドも何もないinterfaceなのでどのようなクラスにも使うことができます。
今回試していた内容ではRetrofitのServiceインターフェースで使いました。

object API : KoinComponent {
  var apiKey: String = ""
  val service: Service by inject{ parameterOf(apiKey) }
}

テスト

DIしたからにはテストをしなければなりません。なんのためにDIしたのかわかりませんから。
テストするにはテストクラスでKoinTestインターフェースを継承します。

class MyTest: KoinTest {
  @Test
  fun 例外なく動く() {
    startKoin(listOf(module {
       factory<MyContract.Presenter> {
          mock(MyContract.Presenter::class.java)
       }

       single<MyContract.Repository> {
          mock(MyContract.Repository::class.java)
       }
    }))
  }
}

継承したクラスではテスト実行時にApplicationクラスで実行していたstartKoinメソッドで初期化します。
またインスタンスをモック化したいのであればmockitoを使ってインスタンスを生成すれば良さそうです(時間がなくて未検証)

AndroidTestの場合

koinもMockitoを使っているらしく設定がややこしい。

build.gradleの抜粋。AndroidTestを実行するのに必要なところだけを抜き出している。

android {
    packagingOptions {
        pickFirst 'mockito-extensions/org.mockito.plugins.MockMaker'
    }
}

dependencies {
    androidTestImplementation ("org.koin:koin-test:1.0.2") { exclude group: 'org.mockito' }
    androidTestImplementation "org.mockito:mockito-android:2.24.5"
}

コンパイル時にこんなエラーが出ていて調べたら下のissueが見つかったので使っている。 More than one file was found with OS independent path 'mockito-extensions/org.mockito.plugins.MockMaker' github.com

それでも直らずkoin mockitoでpackagingOptionsを使う方法が書かれていたので使った。 github.com

さらにMockitoがAndroidTestでも使えるようになっていた。それがorg.mockito:mockito-android:2.24.5 またMockitoがAndroidTestでも使えるようになったことでテストの書き方も変わっている。

テストランナーにはMockitoJUnitRunnerを使うことでよいようだ。 今回はKoinを使ったテストも行うためKoinTestインターフェースを継承したテストを作成している

@RunWith(MockitoJUnitRunner::class)
class MyFragmentTest : KoinTest {
    ...
}