MotifはUberが開発しているDIライブラリで、SimpleなAPIが特徴です。
JSR-269(Annotation Processing)を使ってDagger2のコードを生成してくれるDagger2のwrapperといえます。
本稿執筆にあたってv0.0.10
を参考にしています。
uber/motif
A simple DI API for Android / Java. Contribute to uber/motif development by creating an account on GitHub.
Motifを使ってみる
Scope
まずはScopeを定義します。
ScopeはDagger2における@Component
に相応します。
@motif.Scope
interface AppScope{
}
Objects
つぎに、Scope内にObjectsを定義します。
ObjectsはDagger2における@Module
、Objects内のfactory functionは@Provides
に相応します。
@motif.Scope
interface AppScope{
@motif.Objects
abstrac class Objects {
fun api() = Api()
fun db() = Db()
}
}
Dagger2のprovider functionがそうであったように、Motifのfactory functionはfactory functionの定義されている型を引数に取ることができます。
@motif.Scope
interface AppScope{
@motif.Objects
abstrac class Objects {
fun api() = Api()
fun db() = Db()
fun repository(api: Api, db: Db) = Repository(api, db)
}
}
factory functionはさらにシンプルにすることができます。
Api
とDb
のコンストラクタは引数がないのでMotifにfactory functionの実装を任せることが可能です。
Repository
に関しても、Api
とDb
はすでにfactory functionが定義されているのでMotifに実装を任せることが可能です。
@motif.Scope
interface AppScope{
@motif.Objects
abstrac class Objects {
abstract fun api(): Api
abstract fun db(): Db
abstract fun repository(): Repository
}
}
(余談ですが引数なしabstracfactory functionだけであればinterfaceにすることも可能です)
ChildScope
child scopeはDaggerにおける@Subcomponent
に似た機能ですが、実際には@Component
とComponentのdependencies
機能を使って実装されます。
child scopeを定義したら、Scopeに対してchild functionを定義してchild scopeを指定してあげます。
@motif.Scope
interface AppScope{
val uiScope: UiScope
@motif.Objects
abstrac class Objects {
abstract fun api(): Api
abstract fun db(): Db
abstract fun repository(): Repository
}
}
@motif.Scope
interface UiScope {
@motif.Objects
abstract class Objects {
abstract fun useCase(repository: Repository) = UseCase(repository)
}
}
さて、ここがDaggerの@Subcomponent
の違いになります。実際にはdependencies
を利用しているので、親から子へ公開される依存性は限定されます。
factory functionに@Expose
を指定することでchild scopeに対して依存性を渡すことが可能になります。
UiScopeではApiとDbを扱うことができなくなります。LayeredArchitectureを採用している場合なんかは嬉しいんじゃないでしょうか。
@motif.Scope
interface AppScope{
val uiScope: UiScope
@motif.Objects
abstrac class Objects {
abstract fun api(): Api
abstract fun db(): Db
@Expose abstract fun repository(): Repository
}
}
@motif.Scope
interface UiScope {
@motif.Objects
abstract class Objects {
abstract fun useCase(repository: Repository) = UseCase(repository)
}
}
Use Scopes
以上のScope定義からMotifが実装クラスを生成してくれるので、ApplicationやActivityでインスタンス化します。 Android Support登場前のDaggerを思い出しますね☺️
class App: Application() {
val appScope: AppScope by lazy { AppScopeImpl() }
}
class MainActivity: AppCompatActivity() {
val uiScope: UiScope by lazy { (application as App).appScope.uiScope }
}
Inject
どうやらMotifはまだsetter injectionに対応していないようなので、scopeにaccessorを定義して自分でinjectしなければならないようです。
このようにpropertyを定義すると実装してくれます。
@motif.Scope
interface UiScope {
val useCase: UseCase
@motif.Objects
abstract class Objects {
abstract fun useCase(): UseCase
}
}
あとはpropertyを利用して代入すればよいです。
なんなら、Scopeのインスタンスが同じであれば提供されるインスタンスは毎度おなじなのでscopeだけ持っておくのでも良いかもしれませんね。
class MainActivity: AppCompatActivity() {
val uiScope: UiScope by lazy { (application as App).appScope.uiScope }
val useCase by lazy { uiScope.useCase }
}
ScopeにContextなどを渡したい
Scopeに外からobjectをのせたい場合にはScopeFactoryを作ると良いようです。
@motif.Scope
interface AppScopeFactory {
fun appScope(context: Context): AppScope
}
Daggerとの相互運用性
DaggerとMotifは相互運用できます。@see Dgger Integration
共存させるのはMotifのメリットであるところのシンプルさを失いそうなのでスルー。
Androidアプリ開発で本番投入できるか?
実態はDagger2なのでcompile-timeに依存性の解決を行ってくれているので安心かつruntimeでのパフォーマンスが良いです。
使用するAnnotationが少なく、学習コストがかなり抑えられます。
何個もファイルを作らなくて良いので楽です(これは私が面倒くさがりなだけ)。
ただし、AndroidArchitectureComponent ViewModelを利用しているとちょっと厳しそう。Daggerでごにょごにょするのに比べてViewModelを扱うのがしんどそうです。
依存性のライフサイクルはScopeのライフサイクルと同一なので、DaggerにおけるSingleton
もScope
もつけていないプレーンなprovider functionを実現できなさそうなのも痛手です。
とはいえまだv0.0.10。2018年7月頃に開発が始まったばかりなので今後に期待しています(最近コミットが減ってますが)。
Dagger Android Supportは人類が扱うには難しすぎる