はじめに
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
に追加
compile 'com.github.salomonbrys.kodein:kodein-android:3.0.0'
Kodeinモジュールの作成
早速Daggerのモジュール/コンポーネントをKodeinのモジュールに変換していきます。
まずはApplicationModule
から見ていきます。なお、元のコードはJavaですがこの記事ではKotlinに変換しています。
ApplicationModule
インスタンス生成時にコンストラクタで渡されたcontextを提供するだけの単純なモジュールです。
@Module
class ApplicationModule(private val context: Context) {
@Provides
fun provideContext(): Context {
return context
}
}
KodeinではKodein.Moduleブロックでモジュールを定義します。ApplicationModuleは次のように書き替えられます。
fun applicationModule(context: Context) = Kodein.Module {
bind<Context>() with instance(context)
}
bind<Type>() 〜
がDagger 2の@Provides
が付けられたメソッドに相当します。
今回はすでに生成済みのContextインスタンスを提供するためbind<Context>() with instance(context)
と定義します。
次にTasksRepositoryModule
を見ていきます。
@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の機能が用意されており、次のように書くことができます。
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が使われることになります。
次に、これらのモジュールを使うコンポーネントの定義とコンポーネントの構築部分を合わせて見ていきます。
@Singleton
@Component(modules = listOf(TasksRepositoryModule::class, ApplicationModule::class))
interface TasksRepositoryComponent {
val tasksRepository: TasksRepository
}
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
で遅延実行されるように定義することも可能です。
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を使用した場合は次のようになっています。
@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
を自動的に注入しています。
@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
も作成する必要があります。
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()) }
})
}
fun tasksPresenterModule(view: TasksContract.View) = Kodein.Module {
bind<TasksContract.View>() with instance(view)
}
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の不安定さから少しでも解放されるため、趣味のアプリは順次置き換えていこうと思います。