66
Help us understand the problem. What are the problem?

More than 3 years have passed since last update.

posted at

updated at

Kotlinで DI (Dependency Injection)~ Koin 編

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

はじめに

AndroidアプリにKoinを使ってDIしてみます。
DIがどんなものかというのはこちら

Dagger2を使ったDIはこちら

やりたいこと

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

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

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

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

ViewModelActivityに注入するようにします。

プロジェクトへKoinを導入

まずはこれがないと始まりませんので。

app/build.gradle
dependencies {

    // (省略)

    implementation 'org.koin:koin-android:1.0.2'
}

実装

ViewModel

class MainViewModel {
    fun greet(): String {
        return "こんにちわ"
    }
}

アプリケーションクラス

startKoinでIoCコンテナが生成されるみたいです。
コンテナの中身はモジュール単位でロード可能で、Koin DSL の moduleを使って生成します。
モジュールが提供するインスタンス(もしくは、インスタンスを返す何か)をfactoryに設定します。

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

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属性を追加します。

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にはコンストラクタインジェクションができないのでこうするようです。

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()}")
    }
}

こういう書き方もできます。

private val viewModel by inject<MainViewModel>()

結果

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

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: _/_/ こんにちわ

依存関係を追加するとどうなるか

ViewModelRepositoryに依存するようにして、その RepositoryViewModelに注入してみようと思います。

こんな感じです。

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

Repository

class GreetingRepository {
    fun greet(): String {
        return "こんにちわ from Repository"
    }
}

ViewModel

コンストラクタでGreetingRepositoryが注入されるように修正します。

class MainViewModel(private val greetingRepository: GreetingRepository) {
    fun greet(): String {
        return this.greetingRepository.greet()
    }
}

アプリケーションクラス

モジュールに格納するインスタンスを追加します。
また、ViewModelのコンストラクタ引数にget()を指定します。
get()を指定すると、Koinが引数の型に一致するインスタンスをコンテナから取得して、自動的に注入してくれます。

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からの値が表示されています。

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

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

interface GreetingRepositoryContract {
    fun greet(): String
}

class GreetingRepository : GreetingRepositoryContract {
    override fun greet(): String {
        return "こんにちわ from Repository"
    }
}

ViewModel

interface MainViewModelContract {
    fun greet(): String
}

class MainViewModel(private val greetingRepository: GreetingRepositoryContract) : MainViewModelContract {
    override fun greet(): String {
        return this.greetingRepository.greet()
    }
}

アプリケーションクラス

モジュールをRepository用とViewModel用とに分割して、
モジュールへのインスタンス格納を"as インタフェース"とします。

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

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の型をインタフェースで指定します。
こうすることで、型(=インタフェース)が一致するインスタンスが自動的に注入されます。

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からの値が表示されています。

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の場合と比較して、非常に簡単にDIできました。

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

とりあえずは以上です。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Sign upLogin
66
Help us understand the problem. What are the problem?