Android アプリ の DI コンテナとして KOIN を使う理由とちょこっとした紹介

たまには飯テロじゃない画像をトップにしようと思いました。
土善旅館のはなちゃんです)

f:id:bps_tomoya:20180716143728j:plain

さて、私が Android アプリケーションを作るときの DI コンテナとして、最近は Kotlin で書かれた、Kotlin のために提供される KOIN という DI コンテナを好んで使っています。

beta.insert-koin.io

今回は私が KOIN を使う理由を説明しながら、KOIN の特徴についてほんの少しご紹介します。

様々なプラットフォームの入門が提供されている

どんな道具を使うにしても私達はまず Getting Started に目を通すことになりますが、KOIN はその最初に目を通すドキュメントとして、さまざまなプラットフォーム用の手引き書を提供しています。

beta.insert-koin.io

  • Android アプリ
  • AAC ViewModel を使った Android アプリ
  • ライフサイクルに追従して依存解決をする Android アプリ
  • Kotlin Application
  • JUnit
  • Spark Framework
  • Ktor

Android で KOIN を試してみたいという場合には Android の入門を読むことができるし、すぐにでも AAC での使い方が知りたければ AAC の入門を読んでも構いません。もちろん、純粋に KOIN のことを学びたい場合には Kotlin application を読むのがベストです。

AAC ViewModel を注入する機能が提供されている

Google I/O 2017 で発表された Android Architecture Components には、ViewModel という View のデータを保持する役割等を担うクラスが盛り込まれています。

developer.android.com

KOIN はこの AAC ViewModel を注入するための仕組みを提供しています。

まず、あらかじめ用意した ViewModel クラスを注入する module を定義します。

ViewModelModule.kt

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

val viewModelModule: Module = module {
    viewModel { LoginViewModel() }
}

ここでは LoginViewModel というクラスの注入を定義しました。
このとき LoginViewModel が AAC ViewModel を継承していないクラスになっていると、コンパイル時にエラーとして教えてくれます。

f:id:bps_tomoya:20180721105948p:plain

アプリを実行して初めて気がつく、というケースが減りますね。

さて、module の定義では viewModel という KOIN が提供するキーワードが登場しました。viewModel キーワードは内部で factory というまた別のキーワードを使って、指定された ViewModel クラスのインスタンスを生成しています。

https://github.com/InsertKoinIO/koin/blob/master/koin-android-architecture/src/main/java/org/koin/android/architecture/ext/LifecycleOwnerExt.kt

この処理は最終的に、依存注入の定義をリストアップしている arrayListOf<BeanDefinition<*>> 型の変数への追加を行います。

定義した ViewModel の注入は、View 側 (Activity / Fragment) で以下の様に利用することができます。

LoginFragment.kt

import android.support.v4.app.Fragment
import org.koin.android.viewmodel.ext.android.viewModel

class LoginFragment : Fragment() {
  private val viewModel: LoginViewModel by viewModel()

  // 以下、onCreateView の処理など
}

通常、AAC ViewModel のインスタンス化は以下の様に行っていました。

val viewModel = ViewModelProviders.of(this).get(LoginViewModel::class.java)

KOIN の viewModel キーワードを使うとこの処理を内部的に行ってくれるので、利用者側はそれを意識する必要はありません。

LifecycleOwnerExt.kt

https://github.com/InsertKoinIO/koin/blob/master/koin-android-architecture/src/main/java/org/koin/android/architecture/ext/LifecycleOwnerExt.kt#L146

/**
 * Get a viewModel instance
 *
 * @param fromActivity - create it from Activity (default false) - not used if on Activity
 * @param clazz - Class of the BeanDefinition to retrieve
 * @param key - ViewModel Factory key (if have several instances from same ViewModel)
 * @param name - Koin BeanDefinition name (if have several ViewModel definition of the same type)
 * @param parameters - parameters to pass to the BeanDefinition
 */
fun <T : ViewModel> LifecycleOwner.getViewModelByClass(
    fromActivity: Boolean = false,
    clazz: KClass<T>,
    key: String? = null,
    name: String? = null,
    parameters: Parameters = emptyParameters()
): T {
    KoinFactory.apply {
        this.parameters = parameters
        this.name = name
    }
    val viewModelProvider = when {
        this is FragmentActivity -> {
            Koin.logger.log("[ViewModel] get for FragmentActivity @ $this")
            ViewModelProvider(ViewModelStores.of(this), KoinFactory)
        }
        this is Fragment -> {
            if (fromActivity) {
                Koin.logger.log("[ViewModel] get for FragmentActivity @ ${this.activity}")
                ViewModelProvider(ViewModelStores.of(this.activity), KoinFactory)
            } else {
                Koin.logger.log("[ViewModel] get for Fragment @ $this")
                ViewModelProvider(ViewModelStores.of(this), KoinFactory)
            }
        }
        else -> error("Can't get ViewModel on $this - Is not a FragmentActivity nor a Fragment")
    }
    return if (key != null) viewModelProvider.get(
        key,
        clazz.java
    ) else viewModelProvider.get(clazz.java)
}

引数をとった注入が簡潔

他の DI コンテナでも同様の機能が提供されているので理由付けとしてはやや弱いかも知れませんが、簡潔に定義できる点が気に入っているので紹介します。

https://beta.insert-koin.io/docs/1.0/documentation/reference/index.html#_declaring_injection_parameters

factory キーワードで、引数を取る注入を定義します。

AppModule.kt

val AppModule= module {
  // ComponentA は String 型の url を引数に取るクラス
  factory { (url: String) -> ComponentA(url) }
}

シングルトンにインスタンスが生成される single キーワードでもエラーにはならないのですが、これは初回生成後、いくら引数を与えても初回生成で与えた引数が反映されたインスタンスが注入されてしまうので気を付けてください。私は見事に嵌まってしまいました。

注入を受けるクラスでは以下のようにすることで、引数を与えながら ComponentA のインスタンスを注入してもらうことができます。

AppComponent.kt

class AppComponent : KoinComponent {
  private val componentA: ComponentA by inject { parametersOf("http://example.com") }
}

Android アプリケーションの場合、Activity や Fragment クラスは KoinComponent を継承させる必要はないのですが、その他のクラスについては KoinComponent を継承していないと inject キーワードなどが利用できないので注意が必要です。

https://beta.insert-koin.io/docs/1.0/documentation/reference/index.html#_unlock_the_koin_api_with_koincomponents

依存の注入を視覚的に確認できる

アプリケーションを実行していると、KOIN による依存の注入が開発者の意図したとおりに行われているか、Logcat で視覚的に確認することができます。

f:id:bps_tomoya:20180721121028p:plain

この例では、LoginViewModel は GetTokenUseCase と LoginUseCase が注入され、さらにそれら 2つの UseCase に注入されているクラス、その下の…という注入が行われていることを把握することができます。

まとめ

私がAndroid アプリケーションを作るときの DI コンテナに KOIN を使う理由として4つの項目を紹介しました。

  • 入門資料が幅広く整備されている
  • AAC ViewModel の注入を行う機能が提供されている
  • 引数を与えた注入が簡潔に行える
  • 依存の注入を視覚的に確認できる

私の周りでは KOIN を使っているという人がほぼ居なくてちょっと寂しいのですが…これをきっかけに、少しでも興味を、あわよくばご自身の手を動かして KOIN の使い心地を体験してくれれば幸いです。