1. chibatching

    Posted

    chibatching
Changes in title
+Dagger 2 から Kotlin製DIコンテナ Kodein へ乗り換える
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,236 @@
+## はじめに
+
+Kotlinでkaptを使うライブラリを使用していると消耗するという話をよく聞きます。
+自分自身[Dagger 2 + Databinding + Ormaの環境で激しく消耗](http://qiita.com/chibatching/items/e5a0f5016093abec884b)しましたし、最近ではDaggerを2.3より新しいバージョンにするとビルドができなくなる状況に悩まされています。
+そこで、少しでもkaptへの依存ライブラリを減らすため、kapt不要のKotlin製DIコンテナであるKodeinを試してみたのでその記録。
+
+## Kodeinとは
+
+ドキュメント: [Kodein](https://salomonbrys.github.io/Kodein/)
+リポジトリ: [SalomonBrys/Kodein: Painless Kotlin Dependency Injection](https://github.com/SalomonBrys/Kodein)
+
+Kotlinで作られたDIコンテナです。Kotlin製DI コンテナとしては[injekt](https://github.com/kohesive/injekt)というものがありましたが、こちらの開発は終了し開発者もKodeinの開発に合流しています。
+annotation processorを使わずKotlinの言語機能でDIコンテナを実現することを目的にしているようです。
+
+## Dagger 2 からの乗り換え
+
+今回は単にKodeinを導入するのではなく、Dagger 2を使った既存のプロジェクトをKodeinに乗り換える観点で紹介しようと思います。
+
+### 題材
+
+[android-archtecture](https://github.com/googlesamples/android-architecture)という google 公式 MVP アーキテクチャサンプルリポジトリに Dagger を使用したバージョンがあります。
+今回はこの todo-mvp-dagger の Dagger を Kodein に置き換えていきます。
+
+[googlesamples/android-architecture](https://github.com/googlesamples/android-architecture/tree/todo-mvp-dagger)
+
+
+## インストール
+
+いつものように `build.gradle` に追加
+
+```groovy:build.gradle
+compile 'com.github.salomonbrys.kodein:kodein-android:3.0.0'
+```
+
+## Kodeinモジュールの作成
+
+早速Daggerのモジュール/コンポーネントをKodeinのモジュールに変換していきます。
+まずは`ApplicationModule`から見ていきます。なお、元のコードはJavaですがこの記事ではKotlinに変換しています。
+
+`ApplicationModule`インスタンス生成時にコンストラクタで渡されたcontextを提供するだけの単純なモジュールです。
+
+```kotlin:ApplicationModule.kt
+@Module
+class ApplicationModule(private val context: Context) {
+
+ @Provides
+ fun provideContext(): Context {
+ return context
+ }
+}
+```
+
+KodeinではKodein.Moduleブロックでモジュールを定義します。ApplicationModuleは次のように書き替えられます。
+
+```kotlin: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`を見ていきます。
+
+```kotlin: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の機能が用意されており、次のように書くことができます。
+
+```kotlin: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?`なので他のオブジェクトを使用することも可能です。
+ひとつ目の`bind`で`TasksLocalDataSource`のコンストラクタに`instance()`を渡していますが、これはKodeinで定義された他の`bind`が提供するインスタンスを使用することを意味します。
+今回の場合、`ApplicationModule`でbindしたContextが使われることになります。
+
+次に、これらのモジュールを使うコンポーネントの定義とコンポーネントの構築部分を合わせて見ていきます。
+
+```kotlin:TasksRepositoryComponent.kt
+@Singleton
+@Component(modules = listOf(TasksRepositoryModule::class, ApplicationModule::class))
+interface TasksRepositoryComponent {
+
+ val tasksRepository: TasksRepository
+}
+```
+
+```kotlin: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`で遅延実行されるように定義することも可能です。
+
+```kotlin: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を使用した場合は次のようになっています。
+
+```kotlin: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`を提供し、コンストラクタインジェクションで`TasksPresenter`に`TasksContract.View`と`TasksRepository`を自動的に注入しています。
+
+```kotlin
+@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ではコンストラクタに自動的に依存性を注入することができないので、`TasksPresenter`の`bind`も作成する必要があります。
+
+```kotlin: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()) }
+ })
+}
+```
+
+```kotlin:TasksPresenterModule.kt
+fun tasksPresenterModule(view: TasksContract.View) = Kodein.Module {
+ bind<TasksContract.View>() with instance(view)
+}
+```
+
+```kotlin: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クラスで定義した`TasksRepository`や`Context`をinjectしているKodeinで使用することができます。
+
+## おわりに
+
+実際に、Dagger 2からKodeinへ乗り換えてみることで、Kodeinで何が実現できるのかDagger 2と何が違うのかざっくりとですが理解することができました。
+その仕組み上、Dagger 2のようにコンパイル時に依存性の間違いを検出することができず実行時にエラーとなってしまうため、大規模なアプリでは少々導入が難しいかもしれませんが、概ね自分に必要な機能はそろっているし理解も難しくないという印象です。
+
+kaptの不安定さから少しでも解放されるため、趣味のアプリは順次置き換えていこうと思います。