ticktakclockの日記

技術ポエムを綴ったりします。GitHub idも同じです (@ticktakclock)

Koinを使って依存解決(DI)する

こんにちは、tkyです。

今回は

今までDI(Dependency Injection)ライブラリはDagger2だけしか使ったことがなかったのですが、他のDIライブラリも使ってみたくて Koin を使ってみました。

公式見ながら作業しましたが、想像以上に簡単にDIできたのでびっくり。。。

以下のような2つの数字を足し算するだけのアプリでお試し実装してみました。

f:id:ticktakclock:20190923173251g:plain:w200
サンプル画面

作成したものはGitHubに置いてありますのでご確認ください。

アプリ自体はMVVMを採用し、DataBindingを活用していますが、Koinに特化して記載するため、Bindingについては触れません。 github.com

環境情報

Koin

Kotlin用の軽量な依存注入フレームワーク(日本語訳)です。2019/09/23時点の最新は v2.0.1 となっています。

insert-koin.io

使い方

Koin自体は超らくちんで、App にKoin使う宣言して、モジュールの宣言をするだけです。

app.gradle

    implementation 'org.koin:koin-android:2.0.1'
    implementation 'org.koin:koin-android-scope:2.0.1'
    implementation 'org.koin:koin-android-viewmodel:2.0.1'

App.kt

import android.app.Application
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin

class App : Application() {
    override fun onCreate() {
        super.onCreate()
        // ↓これだけ!DSLでかけるのがいいですね!
        startKoin {
            androidContext(this@App)
            modules(myModule)
        }
    }
}

modules() に依存を解決したいモジュールの宣言をしていきます。

今回は KoinInjector.kt ファイルを作って、その中に変数を定義して見ました。 CalculatorCalculateServiceMainViewModel については後述します。

import org.koin.android.viewmodel.dsl.viewModel
import org.koin.dsl.module

// ちゃんと定数とわかるような命名規則のほうが良いです。
val myModule = module {
    single { Calculator() }
    single { CalculateService(get()) }
    viewModel { MainViewModel(get()) } // ViewModel用のモジュール宣言
}

後出しになりますが、サンプルアプリはクラス間で以下のような依存関係にあります。

f:id:ticktakclock:20190923193025p:plain:w100
依存関係図

Calculator クラスは他のクラスに依存しない計算ロジックのクラスです。

myModule には single { Calculator() } のようにして依存解決します。

class Calculator {
    fun sum(a: Int, b: Int): Int = a + b
}

CalcurateService クラスは計算に関する処理をまとめるためのサービスクラスのような扱いで一枚かませました。DIしたかったし😅

実際、 CalcurateService はRepository(DataSource)層、 Calculator は Dao層のような関係で理解すると良いかもしれません。 Calculator に依存していますが、Calculatorインスタンスはコンストラクタから注入します。

myModule には single { CalculateService(get()) } のようにして依存解決します。

class CalculateService(private val calculator: Calculator) {
    fun sum(a: Int, b: Int): Int = calculator.sum(a, b)
}

MainViewModelCalculateService に依存しています。 これもコンストラクタからインスタンスを注入します。

myModule には viewModel { MainViewModel(get()) } のようにして依存解決します。

import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel

class MainViewModel(private val calculateService: CalculateService) : ViewModel() {

    private var _result: MutableLiveData<String> = MutableLiveData()
    val result: LiveData<String>
        get() = _result

    fun calculate(a: String?, b: String?) {
        val numA = if (a.isNullOrEmpty()) 0 else a.toInt()
        val numB = if (b.isNullOrEmpty()) 0 else b.toInt()
        val result = calculateService.sum(numA, numB).toString()
        _result.value = result
    }
}

MainActivityMainViewModel に依存しています。 MainActivityAndroidクラスにつき、コンストラクタから注入できません。 その代わりKoinが遅延初期化の仕組みを用意してくれています。

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
// import org.koin.android.ext.android.get
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)
        // 関数内でもこのように依存注入できます。
        // val viewModel: MainViewModel = get()
    }
}

これで依存解決されたMainViewModelのインスタンスが出来上がりです!

まとめ

コードを貼り付けただけでしたが、やることは

  1. App.ktに以下を書く
startKoin {
    androidContext(this@App)
    modules(myModule)
}
  1. モジュール(myModule)の依存を解決する
val myModule = module {
    single { Calculator() }
    single { CalculateService(get()) }
    viewModel { MainViewModel(get()) }
}
  1. ActivityでDIする
class MainActivity : AppCompatActivity() {
    private val viewModel: MainViewModel by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        ・・・略・・・
    }
}

たった3つ!って思うとやってみたくなりません?

公式にもGet Startedがあるので見てみてください。