TRY ANDROID DEV

Android アプリ開発のコーディングネタ。あとエンジニアとしての活動

Interfaceの実体クラスにまたInterfaceがある場合のDI(Dagger2)

背景

以下の構成を考える。 f:id:off2white:20190103095800j:plain

ドメイン層は他の層と依存関係を持たないが、ドメインのメソッドで通信を行いたい。 そのためにinterfaceを設定してリポジトリ層と疎結合にする。 ただしリポジトリ層でも通信をする場合とテスト用にローカルファイルを返却する場合で切り替えたい。
そのためInterfaceの実体クラス内にまたInterfaceがある状況になっている。

この構成時にDagger2でDIしたい。

環境

実装

以下の4クラスを実装する

RepositoryComponent

package c.offwhite.novel.di

import c.offwhite.novel.domain.ISearchRepository
import dagger.Component

@Component(modules = [RepositoryModule::class, ApiModule::class])
interface RepositoryComponent {
    fun inject(): ISearchRepository
}

RepositoryModule

@Module
class RepositoryModule {

    @Provides
    fun provideNarouRepository(narouApi: INarouOpenApi): ISearchRepository {
        return NarouRepository(narouApi)
    }
}

ApiModule

@Module
class ApiModule {
    @Provides
    fun provideNarouApi(narouOpenApi: NarouOpenApi): INarouOpenApi {
        return narouOpenApi
    }
}

WebSite

class WebSite (val domain: String) {

    @Inject lateinit var searchRepository: ISearchRepository

    fun search(word: String): List<NovelIntroduction> {
        searchRepository = DaggerRepositoryComponent.create().inject()
        return searchRepository.search(word)
    }
}

解説

Dagger2が依存性を解決できないパターンが複数存在する - Interface(何が具体化されるかわからないため) - サードパーティ製のクラス(@Injectが書いてないため)

これらのDagger2だけで自動的に解決できない依存性を満たすために、こちらから供給するクラス(Factoryクラス)を指定する必要がある。
図のように「もしISearchRepositoryというinterfaceが登場したら、NarouRepositoryを注入する」といった形である。

f:id:off2white:20190103172408j:plain

供給するクラスを指定するクラスをmoduleと呼び、以下のように定義する。

@Module
class RepositoryModule {

    @Provides
    fun provideNarouRepository(narouApi: INarouOpenApi): ISearchRepository {
        return NarouRepository(narouApi)
    }
}

モジュールクラスには@Moduleを記述し、供給メソッドには@Providesを記述する。 @Providesで指定するメソッドは対象のinterfaceを返却するメソッドとし、注入する実体クラスを指定する。
もし供給するRepositoryを切り替えたくなった場合はここのreturnの値を切り替えれば良い。

今回のケースでは実体クラスNarouRepositoryはApIインスタンスを注入する必要がある。 もちろんここで以下のように実体クラスを指定しても良い。

実体クラスを指定した場合

@Module
class RepositoryModule {

    @Provides
    fun provideNarouRepository(): ISearchRepository {
        return NarouRepository(NarouOpenApi())
    }
}

しかし今回はApiインスタンスも条件によって切り替えたいため、このモジュール一つにまとめてしまうと責務が分割できない。 よってここのパラメータをInterface指定としている。 こうなるとまたこのInterfaceの依存を指定する必要があるので、Apiクラスのモジュールも作成する。

ApiModule

@Module
class ApiModule {
    @Provides
    fun provideNarouApi(narouOpenApi: NarouOpenApi): INarouOpenApi {
        return narouOpenApi
    }
}

あとはComponentクラスに「依存注入がわからなくなったらこのmoduleを参照するように」と指定するだけで良い。

RepositoryComponent

package c.offwhite.novel.di

import c.offwhite.novel.domain.ISearchRepository
import dagger.Component

@Component(modules = [RepositoryModule::class, ApiModule::class])
interface RepositoryComponent {
    fun inject(): ISearchRepository
}

以下の@Coomponent(modules = ...)が該当箇所にあたる。ここはリストでの指定が必要なので一つでもリスト形式で記述する。 この指定によって、Daggerが依存性を解決できなかった時に参照する先を指定できる。

@Component(modules = [RepositoryModule::class, ApiModule::class])

あとは以下のドメインクラスにて指定して利用するだけになる。

WebSite

class WebSite (val domain: String) {

    @Inject lateinit var searchRepository: ISearchRepository

    fun search(word: String): List<NovelIntroduction> {
        searchRepository = DaggerRepositoryComponent.create().inject()
        return searchRepository.search(word)
    }
}

これでInterfaceの実体クラス内にまたinterfaceがある場合でもDIできた。

個人的に詰まったところ

  • kolint-kapt がないとkotlinがjavaのannotation processingをサポートしてくれないので、DaggerComponentが作成されない。 そのためgradleに以下の記述が必須。
apply plugin: 'kotlin-kapt'