Kotlinで DI （Dependency Injection）~ Koin 編

(2019年3月1日変更)
バージョン `1.0.2` に対応した記述に変更しました。

## はじめに

Androidアプリに[Koin](https://insert-koin.io/)を使ってDIしてみます。
DIがどんなものかというのは[こちら](https://qiita.com/sudachi808/items/364add4e96a8d6edc82b)。

[Dagger2](https://github.com/google/dagger)を使ったDIは[こちら](https://qiita.com/sudachi808/items/a05237e1294639ea41dd)。

## やりたいこと

`ViewModel`に依存する`Activity`があって...

```
+-----------+
| Activity  |
+-----+-----+
      |
      ▽
+-----------+
| ViewModel |
+-----------+
```

`ViewModel`からデータを貰うようなアプリを作ります。

```
+-----------+    +-----------+
| Activity  |    | ViewModel |
+-----+-----+    +-----+-----+
      |                |
      |     greet()    |
      +--------------->|
      |<---------------+
      |      Hello     |
```

`ViewModel`を`Activity`に注入するようにします。

## プロジェクトへKoinを導入
まずはこれがないと始まりませんので。

```groovy:app/build.gradle
dependencies {

    // (省略)

    implementation 'org.koin:koin-android:1.0.2'
}
```
## 実装
### ViewModel
```kotlin
class MainViewModel {
    fun greet(): String {
        return "こんにちわ"
    }
}
```
### アプリケーションクラス
`startKoin`でIoCコンテナが生成されるみたいです。
コンテナの中身はモジュール単位でロード可能で、Koin DSL の `module`を使って生成します。
モジュールが提供するインスタンス（もしくは、インスタンスを返す何か）を`factory`に設定します。

ちなみに`factory`の代わりに`single`を使うとシングルトンになります。


```kotlin
import org.koin.android.ext.android.startKoin
import org.koin.dsl.module.applicationContext
import org.koin.dsl.module.Module

class Application : android.app.Application() {

    override fun onCreate() {
        super.onCreate()

        // Koinコンテナ生成
        startKoin(this, listOf(
                this.module
        ))
    }

    // Koinモジュール
    private val module: Module = module {
        factory { MainViewModel() }
    }
}
```
### マニフェスト
アプリ起動時のクラスが`Application`になるように`android:name`属性を追加します。

```xml:AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mykoinapp">

    <application
        android:name=".Application"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <activity android:name=".MainActivity">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
```
### アクティビティ
ViewModelを代入するプロパティを宣言しておいて、`by inject()`を付けておきます。
こうしておくと、コンテナから型が一致するインスタンスが注入されます。

Activityにはコンストラクタインジェクションができないのでこうするようです。

```kotlin
import org.koin.android.ext.android.inject

class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModel by inject()  // <- これ

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        println("_/_/ ${this.viewModel.greet()}")
    }
}
```
こういう書き方もできます。

```kotlin
private val viewModel by inject<MainViewModel>()
```

### 結果
こんな感じでログが表示されました。
Koinのログも出力されていますね。
コンテナからインスタンスが取り出されている（というか`factory`なので生成されている）ことが確認できます。

```text:logcat
06-25 04:38:50.089 11318-11318/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.MainViewModel] with Factory[class=com.example.mykoinapp.MainViewModel]
06-25 04:38:50.093 11318-11318/com.example.mykoinapp I/KOIN: (*) Created
06-25 04:38:50.094 11318-11318/com.example.mykoinapp I/System.out: _/_/ こんにちわ
```

## 依存関係を追加するとどうなるか
`ViewModel`が`Repository`に依存するようにして、その `Repository`を`ViewModel`に注入してみようと思います。

こんな感じです。

```
+-----------+    +-----------+    +-------------+
| Activity  |    | ViewModel |    | Repository  |
+-----+-----+    +-----+-----+    +------+------+
      |                |                 |
      |     greet()    |                 |
      +--------------->|     greet()     |
      |                +---------------->|
      |                |<----------------+
      |<---------------+     Hello       |
      |     Hello      |                 |
```
### Repository
```kotlin
class GreetingRepository {
    fun greet(): String {
        return "こんにちわ from Repository"
    }
}
```
### ViewModel
コンストラクタで`GreetingRepository`が注入されるように修正します。

```kotlin
class MainViewModel(private val greetingRepository: GreetingRepository) {
    fun greet(): String {
        return this.greetingRepository.greet()
    }
}
```
### アプリケーションクラス
モジュールに格納するインスタンスを追加します。
また、`ViewModel`のコンストラクタ引数に`get()`を指定します。
`get()`を指定すると、Koinが引数の型に一致するインスタンスをコンテナから取得して、**自動的に注入**してくれます。

```kotlin
class Application : android.app.Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin(this, listOf(
                this.module
        ))
    }

    private val module: Module = module {
        factory { GreetingRepository() }  // <- これ
        factory { MainViewModel(get()) }  // <- 引数に `get()` を指定
    }
}
```
### 結果
こんな感じでログが表示されました。
Repositoryからの値が表示されています。

```text:logcat
06-25 04:45:20.797 11556-11556/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.MainViewModel] with Factory[class=com.example.mykoinapp.MainViewModel]
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/KOIN: 	Resolve class[com.example.mykoinapp.GreetingRepository] with Factory[class=com.example.mykoinapp.GreetingRepository]
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/KOIN: 	(*) Created
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/KOIN: (*) Created
06-25 04:45:20.799 11556-11556/com.example.mykoinapp I/System.out: _/_/ こんにちわ from Repository
```

依存関係が増えた場合でも簡単にDIできました。

## もっと実践的に
「モックを注入できるように」とかあるのと思うので改良してみます。

### Repository
インタフェースを定義して、ユニットテストとかでモックへの差し替えを容易にします。

```kotlin
interface GreetingRepositoryContract {
    fun greet(): String
}

class GreetingRepository : GreetingRepositoryContract {
    override fun greet(): String {
        return "こんにちわ from Repository"
    }
}
```
### ViewModel
```kotlin
interface MainViewModelContract {
    fun greet(): String
}

class MainViewModel(private val greetingRepository: GreetingRepositoryContract) : MainViewModelContract {
    override fun greet(): String {
        return this.greetingRepository.greet()
    }
}
```
### アプリケーションクラス
モジュールをRepository用とViewModel用とに分割して、
モジュールへのインスタンス格納を"`as インタフェース`"とします。

こうしておくと、**インタフェースが一致するインスタンス**がコンテナから出てきます。
もちろん、先述したように、コンストラクタ引数に`get()`を渡すと、型の場合と同じく、インタフェースが一致するインスタンスが**自動で注入**されます。

```kotlin
class Application : android.app.Application() {

    override fun onCreate() {
        super.onCreate()

        startKoin(this, listOf(
                this.repositoryModule,
                this.viewModelModule
        ))
    }

    private val repositoryModule: Module = module {
        factory { GreetingRepository() as GreetingRepositoryContract }
    }

    private val viewModelModule: Module = module {
        factory { MainViewModel(get()) as MainViewModelContract }
    }
}
```
モジュールの分割方針は、開発規模や担当者の分担具合によっていろいろあると思います。
この例では処理レイヤーで分割してみました。

### アクティビティ
注入するViewModelの型をインタフェースで指定します。
こうすることで、型（＝インタフェース）が一致するインスタンスが自動的に注入されます。

```kotlin
class MainActivity : AppCompatActivity() {

    private val viewModel: MainViewModelContract by inject() // <-　型にインタフェースを指定

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        println("_/_/ ${this.viewModel.greet()}")
    }
}
```

### 結果
こんな感じでログが表示されました。
変わり映えしませんが、Repositoryからの値が表示されています。

```text:logcat
06-25 05:07:38.525 12512-12512/com.example.mykoinapp I/KOIN: Resolve class[com.example.mykoinapp.MainViewModelContract] with Factory[class=com.example.mykoinapp.MainViewModelContract]
06-25 05:07:38.527 12512-12512/com.example.mykoinapp I/KOIN: 	Resolve class[com.example.mykoinapp.GreetingRepositoryContract] with Factory[class=com.example.mykoinapp.GreetingRepositoryContract]
06-25 05:07:38.527 12512-12512/com.example.mykoinapp I/KOIN: 	(*) Created
06-25 05:07:38.527 12512-12512/com.example.mykoinapp I/KOIN: (*) Created
06-25 05:07:38.528 12512-12512/com.example.mykoinapp I/System.out: _/_/ こんにちわ from Repository
```

## おわりに

[Dagger2の場合](https://qiita.com/sudachi808/items/a05237e1294639ea41dd)と比較して、非常に簡単にDIできました。

どうしてもDagger2を使わなければならない理由がなければKoinの方が簡単で良いんじゃないかなと思います。
たた、コンテナのインスタンス取り出しが動的に解決されるため、「コンテナに追加し忘れた」なんていうのがあると、**実行時エラーの可能性がある**ので気を付けてください。

とりあえずは以上です。