Dagger 2 から Kotlin製DIコンテナ Kodein へ乗り換える

  • 39
    いいね
  • 0
    コメント

はじめに

Kotlinでkaptを使うライブラリを使用していると消耗するという話をよく聞きます。
自分自身Dagger 2 + Databinding + Ormaの環境で激しく消耗しましたし、最近ではDaggerを2.3より新しいバージョンにするとビルドができなくなる状況に悩まされています。
そこで、少しでもkaptへの依存ライブラリを減らすため、kapt不要のKotlin製DIコンテナであるKodeinを試してみたのでその記録。

Kodeinとは

ドキュメント: Kodein
リポジトリ: SalomonBrys/Kodein: Painless Kotlin Dependency Injection

Kotlinで作られたDIコンテナです。Kotlin製DIコンテナとしてはinjektというものがありましたが、こちらの開発は終了し開発者もKodeinの開発に合流しています。
annotation processorを使わずKotlinの言語機能でDIコンテナを実現することを目的にしているようです。

Dagger 2 からの乗り換え

今回は単にKodeinを導入するのではなく、Dagger 2を使った既存のプロジェクトをKodeinに乗り換える観点で紹介しようと思います。

題材

android-archtectureというgoogle公式MVPアーキテクチャサンプルリポジトリにDaggerを使用したバージョンがあります。
今回はこのtodo-mvp-daggerのDaggerをKodeinに置き換えていきます。

googlesamples/android-architecture

追記
実際に乗り換えてみたコードはこちらにあります(READMEなどは書いてませんが..)
chibatching/android-architecture at todo-mvp-kodein

インストール

いつものように build.gradle に追加

build.gradle
compile 'com.github.salomonbrys.kodein:kodein-android:3.0.0'

Kodeinモジュールの作成

早速Daggerのモジュール/コンポーネントをKodeinのモジュールに変換していきます。
まずはApplicationModuleから見ていきます。なお、元のコードはJavaですがこの記事ではKotlinに変換しています。

ApplicationModuleインスタンス生成時にコンストラクタで渡されたcontextを提供するだけの単純なモジュールです。

ApplicationModule.kt
@Module
class ApplicationModule(private val context: Context) {

    @Provides
    fun provideContext(): Context {
        return context
    }
}

KodeinではKodein.Moduleブロックでモジュールを定義します。ApplicationModuleは次のように書き替えられます。

ApplicationModule.kt
fun applicationModule(context: Context) = Kodein.Module {
    bind<Context>() with instance(context)
}

bind<Type>() 〜がDagger 2の@Providesが付けられたメソッドに相当します。
今回はすでに生成済みのContextインスタンスを提供するためbind<Context>() with instance(context)と定義します。

次にTasksRepositoryModuleを見ていきます。

TasksRepositoryModule.kt
@Module
class TasksRepositoryModule {

    @Singleton
    @Provides
    @Local
    fun provideTasksLocalDataSource(context: Context): TasksDataSource {
        return TasksLocalDataSource(context)
    }

    @Singleton
    @Provides
    @Remote
    fun provideTasksRemoteDataSource(): TasksDataSource {
        return FakeTasksRemoteDataSource()
    }
}

ここでは@Local@RemoteのQualifierでタグ付けされている型TasksDataSourceがSingletonで提供されていることがわかります。
Kodeinでもタグ付けやSingletonの機能が用意されており、次のように書くことができます。

TasksRepositoryModule.kt
fun tasksRepositoryModule() = Kodein.Module {
    bind<TasksDataSource>("Local") with singleton { TasksLocalDataSource(instance()) }
    bind<TasksDataSource>("Remote") with singleton { FakeTasksRemoteDataSource() }
}

with singleton { // gen instance }と定義することでSingletonのbindとなります。
また、bind関数の引数にタグを指定することで、同じ型/インターフェースの異なるインスタンスをbindすることが可能です。

今回タグは文字列で付けていますが、bind関数の引数の型はAny?なので他のオブジェクトを使用することも可能です。

ひとつ目のbindTasksLocalDataSourceのコンストラクタにinstance()を渡していますが、これはKodeinで定義された他のbindが提供するインスタンスを使用することを意味します。
今回の場合、ApplicationModuleでbindしたContextが使われることになります。

次に、これらのモジュールを使うコンポーネントの定義とコンポーネントの構築部分を合わせて見ていきます。

TasksRepositoryComponent.kt
@Singleton
@Component(modules = listOf(TasksRepositoryModule::class, ApplicationModule::class))
interface TasksRepositoryComponent {

    val tasksRepository: TasksRepository
}
ToDoApplication.kt
class ToDoApplication : Application() {

    lateinit var tasksRepositoryComponent: TasksRepositoryComponent

    override fun onCreate() {
        super.onCreate()

        tasksRepositoryComponent = DaggerTasksRepositoryComponent.builder()
                .applicationModule(ApplicationModule(applicationContext))
                .tasksRepositoryModule(TasksRepositoryModule())
                .build()
    }
}

Kodeinでは、Dagger 2のコンポーネント定義とコンポーネント生成を合わせたようなものをKodeinブロックで行います。

Applicationクラスの生成後にContextにアクセスするためKodein.lazyで遅延実行されるように定義することも可能です。

ToDoApplication.kt
class ToDoApplication : Application(), KodeinAware {

    override val kodein: Kodein by Kodein.lazy {
        import(tasksRepositoryModule())
        import(applicationModule(this@ToDoApplication))

        bind<TasksRepository>() with singleton { TasksRepository(instance("Remote"), instance("Local")) }
    }
}

KodeinブロックでKodein.Moduleを指定するにはimport関数でモジュールを指定します。

そして、TasksRepositoryを提供するためにモジュールの時と同様に提供する依存をbindしますが、タグ付けされた依存を使うためにinstance関数の引数でタグを指定しています。

AndroidでKodeinを使うときは、ApplicationクラスでKodeinAwareインターフェースを実装しておきます。
そうすると、後述するようにアプリケーションクラスが持つKodeinに他のActivityなどから簡単にアクセスできるようになります。

依存性の注入

次に、上で定義したTasksRepositoryの依存性を注入してみます。

Dagger 2を使用した場合は次のようになっています。

TasksActivity.kt
@Inject
lateinit var mTasksPresenter: TasksPresenter

override fun onCreate(savedInstanceState: Bundle?) {
    // (省略)

    // Create the presenter
    DaggerTasksComponent.builder()
            .tasksRepositoryComponent((application as ToDoApplication).tasksRepositoryComponent)
            .tasksPresenterModule(TasksPresenterModule(tasksFragment))
            .build()
            .inject(this)
}

TasksPresenterModuleではTasksContract.Viewを提供し、コンストラクタインジェクションでTasksPresenterTasksContract.ViewTasksRepositoryを自動的に注入しています。

@FragmentScoped
@Component(dependencies = TasksRepositoryComponent::class, modules = TasksPresenterModule::class)
interface TasksComponent {
    fun inject(activity: TasksActivity)
}

@Module
class TasksPresenterModule(private val view: TasksContract.View) {
    @Provides
    internal fun provideTasksContractView(): TasksContract.View {
        return view
    }
}

class TasksPresenter
@Inject constructor(
        private val tasksRepository: TasksRepository,
        private val tasksView: TasksContract.View) : TasksContract.Presenter {

Kodeinではコンストラクタに自動的に依存性を注入することができないので、TasksPresenterbindも作成する必要があります。

TasksActivity.kt
private val injector = KodeinInjector()
private val tasksPresenter: TasksContract.Presenter by injector.instance()

override fun onCreate(savedInstanceState: Bundle?) {
    // (省略)

    // Create the presenter
    injector.inject(Kodein {
        extend(appKodein())  // appKodein()でApplicationのkodeinを取得できる
        import(tasksPresenterModule(tasksFragment))
        bind<TasksContract.Presenter>() with provider { TasksPresenter(instance(), instance()) }
    })
}
TasksPresenterModule.kt
fun tasksPresenterModule(view: TasksContract.View) = Kodein.Module {
    bind<TasksContract.View>() with instance(view)
}
TasksPresenter.kt
class TasksPresenter(
        private val tasksRepository: TasksRepository,
        private val tasksView: TasksContract.View) : TasksContract.Presenter {

Kodeinではインジェクトの方法がいくつかあるのですが、ここではKodeinInjectorを使用しています。
KodeinInjectorはinject関数を実行したタイミングで、Delegationで指定されたすべてのインスタンスが注入されるため、Daggerのinjectと同じような感覚で使用することができます。

injectしているKodein定義の中でextend(appKodein())としている部分は、Applicationクラスで定義したkodeinを拡張することを宣言しています。
このため、Applicationクラスで定義したTasksRepositoryContextをinjectしているKodeinで使用することができます。

おわりに

実際に、Dagger 2からKodeinへ乗り換えてみることで、Kodeinで何が実現できるのかDagger 2と何が違うのかざっくりとですが理解することができました。

その仕組み上、Dagger 2のようにコンパイル時に依存性の間違いを検出することができず実行時にエラーとなってしまうため、大規模なアプリでは少々導入が難しいかもしれませんが、概ね自分に必要な機能はそろっているし理解も難しくないという印象です。

kaptの不安定さから少しでも解放されるため、趣味のアプリは順次置き換えていこうと思います。