春ぐらいからFRESH LIVEの配信アプリFRESH CASTを開発しています。
FRESH CASTのDIはDagger2 Android Supportを採用しているのですが、試しにKoinに書き換えてみたので共有します。(リリースはしないと思う)
移行については後半書きます。
*参考にしているKoinのバージョンは1.0.1
Koin?
Kotlin製のDIライブラリです。ReflectionとDelegatedPropetiesを使ってdependencyをinjectしてくれます。
Dagger2のようにJSR-269(AnnotationProcessing)によってコードを自動生成するような作りにはなっていません。
したがって、人間が手で依存性を解決するコードを書かなければなりませんが、提供されているExtensionFunction, TopLevelFunction, DSLのおかげで割と手軽に書くことができます。
Module
まず依存性を提供するModuleを定義します。
val module = module {
single { FooApi() }
single { FooDatabase() }
// 型推論あるので実際にはget関数の呼出に型を明示する必要はない
single { FooRepository(get<FooApi>(), get<FooRepository>()) }
viewModel { FooViewModel(get<FooRepository()>) }
factory { Foo() }
}
single関数を使うとsingletonになります。singletonにしたくない場合はfactory関数を使います。AndroidArchitectureComponentsのViewModelに関してはviewModel関数を使います。
Inject
プロパティまたはローカル変数の初期化をinject
関数に委譲することでinjectします。
class FooActivity: AppCompatActivity() {
val foo: Foo by inject()
val viewModel: FooViewModel by viewModel()
}
class FooFragment: Fragment() {
val viewModel: FooViewModel by sharedViewModel()
}
viewModelに関してはinject関数ではなくviewModel
関数を使います。ActicityとFragmentでViewModelを共有する場合はsharedViewModel
関数を使います。
startKoin
ApplicationのonCreateでstartKoin
関数を呼べば完了。
class App: Application() {
override fun onCreate() {
super.onCreate()
startKoin(this, listOf(module))
}
}
このとき依存グラフの正当性がチェックされます。module内で提供されていない型をgetしようとしたり、同じ型を複数定義していたりすると例外が発生します。(同じ型を複数定義するオプションも存在はする)
module {
single { FooApi() }
single { FooApi() } // 重複
single { BarRepository(get<BarApi>()) } // BarApiは提供されていない
}
提供されていない型をinjectしようとしている
のように、実際にinjectするまで評価されないのでチェックできません。
あまり依存グラフが大きいor複雑だとチェックに時間がかかってしまうかもしれません (要検証) 。
Scope
scopeを定義しscope単位でsingletonを実現することも可能です。
module {
scope("scopeName") { FooApi() }
}
val fooApi: FooApi by inject(scope = getOrCreateScope("scopeName"))
scopeを指定する場合はmoduleでscope関数を使い、getOrCreateScope関数
でScope
を取得してinject関数に渡します。module側と利用側でSscopeの指定が必要なのがちょっと怖いです。
scopeを破棄するにはclose関数
を使います。
getOrCreateScope("scopeName").close()
AndroidArchitectureComponentsのLifecycleが利用可能であればこれらを簡略化できます。
module {
scope("scopeName") { FooApi() }
}
class MainActivity: AppCompatActivity() {
val fooApi: FooApi by inject()
override fun onCreate() {
super.onCreate()
bindScope(getOrCreateScope("scopeName"))
}
}
bindScope関数
を利用すれば、inject関数にscopeを渡す必要がなくなります。scopeのcloseも自動で行ってくれます。デフォルトではLifecycle.Event.ON_DESTROY
が指定されており、onDestroyでscopeがcloseされます。他にもLifecycle.Event.ON_STOP
を指定することが可能です。
Dagger2からKoinに移行する
単純な書き換えが多いだけなので大変ではあるが難しくはないです。正規表現でを利用してreplaceしていくと楽。
Dagger2のModuleクラスをすべてKoinのdslで書き換える
@Module class Module {
@Provide fun foo() = Foo()
@Singleton fun bar() = Bar()
}
module {
factory { Foo() }
single { Bar() }
}
@Module
アノテーションのincludes
や@Component
アノテーションのmodules
を利用して複数のModule/複数のファイルに分割していた場合は以下のような方法があります。
@Module(includes = [AnotherModule::class]) class Module
// or
@Component(modules = [Module::class, AnoherModule]) interface Component
val module = module { ... }
val anotherModule { ... }
class App: Application {
override fun onCreate() {
super.onCreate()
startKoint(this, listOf(module, anotherModule))
}
}
val module = module {
subModules += listOf(anotherModule())
}
private val anotherModule { ... }
class App: Application {
override fun onCreate() {
super.onCreate()
startKoint(this, listOf(module))
}
}
Dagger2のConstructorInjectionをKoinのdslで書き換える
class Foo @Inject constructor(private val bar: Bar) {
...
}
Dagger2は依存グラフに存在する型を引数にとるconstructorであれば@Inject
でConstructorInjection可能です。Moduleにprovider関数を定義しなくてよいので非常に便利です。
Koinに移行する場合はこれもsingle|factory|scope
関数を使って書かなければなりません。
ActivityScopeをKoinで実現する
Dagger2でActivityごとにScopeを切っている場合。
cf. Dagger 2でAndroidのライフサイクルごとのSingletonを実現する
koinでもscopeを定義しscope単位でsingletonをし、bindScope関数によってonDestroyでscopeをclose可能なことは前述しました。
FooActivityからBarActivityに遷移するパターンを考えてみましょう。
このとき、FooActivity#onDestroy
よりもBarActivity#onCreate
のほうが先に呼ばれるため、FooActivityでscopeをcloseする前にBarActivityでbindScope(getOrCreateScope("scope name"))
が実行されます。よってFooActivityで利用していたscopeをBarActivityで再利用してしまいます。
以下のような関数を作ることで画面遷移後に強制的にscopeを再生性することは可能です。
fun LifecycleOwner.recreate(name: String) = (this as ComponentCallbacks).getKoin().scopeRegistry.run {
getScope(name)?.close()
createScope(name)
}
上記のような関数を利用する場合、injectにも手を加えたほうが良いとおもいます。
inject関数は以下のように遅延評価のlazyを利用しています。
inline fun <reified T : Any> ComponentCallbacks.inject(
name: String = "",
scope: Scope? = null,
noinline parameters: ParameterDefinition = emptyParameterDefinition()
) = lazy { get<T>(name, scope, parameters) }
scopeが閉じられてからアクセスしてしまうと新しいscopeからインスタンスをinjectすることになってしまうので以下のような手段を取ると良いかもしれません。
scopeに関してはまだベストプラクティスを見つけれていないので他に良い方法があったら教えてほしいです。
class MainActivity: AppCompatActivity() {
lateinit var foo: Foo
override fun onCreate(savedInstanceState: Bundle?) {
...
bindScope(recreate("scopeName"))
foo = get()
// ViewModelに関しても、viewModel関数の内部で呼んでいる関数を利用して初期化を行えばよい。
}
}
// or
class MainActivity: AppCompatActivity() {
val foo: Foo by inject()
override fun onCreate(savedInstanceState: Bundle?) {
...
bindScope(recreate("scopeName"))
foo // 意図的にアクセスすることで評価させる
}
}
複数の型を扱う
interface Foo
とその実装class FooImpl
がある場合を考えてみましょう。
module {
single { FooImpl() }
}
val foo: Foo by inject()
これはRuntimeで例外が発生します。
FooImpl
をFoo
およびFooImple
として提供するにはbind
関数を利用します。
module {
single { FooImpl() } bind Foo::class
}
val foo: Foo by inject()
Dagger2, Koin比較
個人的な意見ですが、Dagger2 Android Supportは難しすぎます。Componentの取得やSubComponentのインスタンス化や、inject関数の呼出などを省けるようにはなりましたが、Processorに渡さなければならない型情報が増えたので黒魔術感が増大しました。(そしてProcessorの吐くエラーはわかりにくい)
Koinはdslでmoduleを記述し、ReflectionとDelegatedPropertiesを使ってinjectを行うだけなのでKotlinに慣れている人であればDagger2に比べて容易/手軽だと思います。
その手軽さの一方で失ったものもあります。
Koinはruntimeに依存グラフの正当性をチェックをします。もし誤りがあった場合にはruntimeでクラッシュしてしまいます。
Dagger2を捨ててKoinに移行すべきか?
あなた次第
私はCompile-timeに依存グラフの正当性をチェックしてくれるほうが好きです。Runtimeで落ちるものはQAが大変なので。
Motifを試してみよう!
UberがDagger2をwrapしたMotifというDIを開発しているので、次はこちらを試してみようと思います。
simple APIを目指しているそうです。