Dagger2のwrapper、Motifを試す

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はさらにシンプルにすることができます。
ApiDbのコンストラクタは引数がないのでMotifにfactory functionの実装を任せることが可能です。
Repositoryに関しても、ApiDbはすでに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におけるSingletonScopeもつけていないプレーンなprovider functionを実現できなさそうなのも痛手です。

とはいえまだv0.0.10。2018年7月頃に開発が始まったばかりなので今後に期待しています(最近コミットが減ってますが)。
Dagger Android Supportは人類が扱うには難しすぎる


Android  DI  Motif