What is it, naokirin?

Kotlin向けDIフレームワーク Koin を使ってみよう

Kotlin は最近は Android で正式採用され、開発に採用される方も増えてきたのではないかと思います(とはいっても Google I/O 2017 なのでもう一昨年の話ですが)。

Androidに限らず、テストを書いたりしながら開発をしていると、DIのような依存性を柔軟に解決できる機構が欲しくなります。

Androidなどでは、Javaでの開発で Dagger2 がよく利用されているようですが、Kotlin っぽい書き方ができるDIフレームワークとして Koin というものがあります。

insert-koin.io

今回はこのKoinについて、Android以外での利用について書いてみます。

ただ基本的な利用方法については、Androidでの場合と同じであり、gradleスクリプトでの dependencies の変更や ViewModel の利用時の違いくらいなので、Androidで利用する場合でも、多少は役に立つかと思います。

これ以降は解説のみを行うため、動作するサンプルは以下を参考にしてください。

github.com

バージョン情報

  • kotlin: 1.3.20
  • koin: 1.0.2
  • gradle: 5.1.1

Koin を使えるように build.gradle を設定する

今回は gradle による導入を行います。

一般的にKotlinをビルドする際に必要な項目は省略して、Koinを導入するのに必要な部分のみを記載します。

repositories {
    // 〜省略 〜

    jcenter()  // 追加
}

dependencies {
    // 〜省略 〜

    // 以下を追加
    implementation 'org.koin:koin-core:1.0.2'
    testImplementation 'org.koin:koin-test:1.0.2'
}

上記を行うことで、Koin を利用できるようになります。

[補足] Androidの場合

Android の場合は以下のように書くと ViewModelに対応した機能の利用や、AndroidTestでの実行が可能になります。

dependencies {
    implementation 'org.koin:koin-android-viewmodel:1.0.2'
    testImplementation 'org.koin:koin-test:1.0.2'
    androidTestImplementation 'org.koin:koin-test:1.0.2'
}

Koin で DI してみる

Koin では、 Module を作成して登録し、 by inject() により、注入を行います。

まず Module を生成します。

val impl_module = module {

    // Singleton を生成する
    // 注入先の型: BaseSingle
    // 実際のオブジェクトの型: SingleImpl
    single<BaseSingle> { ClassSingle() }

    // Factory により生成する
    // 注入先の型: BaseGenerated
    // 実際のオブジェクトの型: GeneratedImpl
    factory<BaseGenerated> { GeneratedImpl() }
}

ここで module { ... } により Module を生成しています。

さらに、Singleton および Factory による生成の登録を行っています。

DSL 機能
single オブジェクトを一度のみ生成し、すべてでそのオブジェクトを注入する(Singleton)
factory 注入するごとにオブジェクトを生成する(Factory)

次に、 by inject() して 依存性の注入を行うコードを書きます。

import org.koin.standalone.KoinComponent
import org.koin.standalone.inject

// inject を行うクラスでは KoinComponent を継承する必要がある
class Injected : KoinComponent {

    // *** dependency injection ***
    val singleObj: BaseSingle by inject()
    val generatedObj: BaseGenerated by inject()
}

by inject() を利用するクラスでは、 KoinComponent を継承する必要があります。 KoinComponent は 実装はありますがインターフェースのため、他のクラスの継承も可能です。

最後に 実際に inject するものを登録するコードを実装します。

import com.example.impl.impl_module
import org.koin.standalone.StandAloneContext.startKoin

fun main(args: Array<String>) {
    startKoin(listOf(impl_module))

   // 〜省略〜
}

startKotlin() に渡した Module で登録されたオブジェクトを利用して by inject() 部分の依存性の解決が行われるようになります。

Koin を使っていく上での Tips

ここでは、実際に使っていく上でよく利用する機能を列挙していきます。

依存関係をネストする( get() )

登録する依存するオブジェクトが、さらに他のオブジェクトに依存している場合です。

Koin ではこのような場合のために、 get() 関数が用意されているため、簡単に依存性のネストを解決できます。

class ClassSingle(val foo: ...) : BaseSingle { ... }

module {
    // 他に登録された依存性のオブジェクトを引数に受け取って生成する
    single<BaseSingle> { ClassSingle(get()) }
}

依存性の上書き(override)

Koinでは依存性を上書きすることができます。

同じクラス、インターフェースに対応する依存性が登録された場合に、以下のようにすることで他の依存性を上書きすることができます。

// モジュール全体で上書き
module(override = true) { ... }

// 特定のものだけ上書き
module {
    single<...>(override = true) { ... }

    factory<...>(override = true) { ... }
}

loadKoinModules()

startKoin() で、どのモジュールを注入するか指定しましたが、 startKoin() は一度しか呼ぶことができない制限があります。

テストなどで複数回呼び出しを行いたい場合や、行う可能性がある場合は loadKoinModules() を利用します。

基本的に startKoin() と同じなのでコードは割愛します。

stopKoin()

stopKoin() を呼び出すと、登録済み、および生成済みのオブジェクトをすべて破棄することができます。

そのためテスト終了時などには呼び出す必要があります。

KoinTest, AutoCloseKoinTest

JUnit によるテストコードの実装をする際に KoinTest もしくは AutoCloseKoinTest を継承したテストクラスで行うことで by inject() を利用してテストを書くことができます。

AutoCloseKoinTest はその名の通り自動で各テストメソッドの終了時に stopKoin() の呼び出しが行われます。

Koin 2.0 での変更

ここまでは 1.x バージョンの Koin をもとに説明してきました。

ですが、すでに Koin では 2.0 バージョンの Beta があり、多少コードの書き方に変更があります。

基本的には startKoin が 単純にモジュールを渡す形式から、スコープで各種設定を行う形式になります。 様々な設定ができるようになっていますが、詳細については公式のドキュメントを参照してみてください。

startKoin {
        modules(moduleList) 
    }

まとめ

Koin を利用することで、プロダクションコードとテストコード時の依存性の違いを簡潔に記述することができます。

Android 用の機能としては Android Architecture Components の ViewModel に対応したものもあります。

すでに必要な機能はかなり揃っているようですが、開発も活発に行われているようなので今後もアップデートされていくかと思います。

Kotlin で DIフレームワークを利用する場合は、ぜひ Koin も検討してみるとよいかと思います。