大規模なAndroidアプリではDI(Dependency Injection; 依存性の注入)ライブラリが組み込まれていることがあります。
多重に依存関係が存在するクラス群の初期化をDIライブラリに担わせることにより、開発者はより本質的なコード実装に時間を使うことが出来ます。

Daggerとは?

Daggerは、Androidアプリ開発におけるDIライブラリとしてよく採用されているライブラリです。
コードのコンパイル時にDIコンテナを自動生成するため、実行時において速いことが謳われています。
現在はGoogleによってメンテナンスされています。

Daggerを導入することにより、以下の点においてメリットがあります。

このCodelabsで学ぶこと

このCodelabsでは、上記で述べたDaggerの恩恵について、実際にコードを書いて体感していく形式をとります。
具体的には以下の内容を収録しています。

必要なもの

このCodelabsの想定読者層

このCodelabsは、Daggerをこれから知りたいと思っているAndroid開発者に向けて制作されたものです。
特に、なんとなくでDaggerを使っている(使うことを強いられている)開発者が自分の力でDaggerを導入できるようになるよう配慮を心がけました。

そのため、このCodelabsでは、Daggerがどういう根拠で動いているのか、どう便利なのか、どのようなときにDaggerを導入するべきなのか、といった文章は一切ありません
まず動作するコードを自力で書いてから、理屈や理論は(他の資料を見て)あとで身につけてもらうというアプローチを採用しました。

本Codelabsは、まず「Daggerが導入されていないアプリをDaggerに対応させる」というロールプレイ方式で、Daggerの良さを手を動かしながら学びます。
後半ではDaggerで起こりがちなトラブルとその対応例を紹介します。

イントロの終わりに

まず序盤から手を動かしてみることで、Daggerに対する「よく分からない感」を払拭できればいいなと思います。
そして、後半を進めていくことで、Daggerをより使いこなし、日常業務でも複雑な処理を苦もなく実現できるようになれば、制作者冥利に尽きます。

さぁ、始めましょう!

プロジェクトのclone

まずはCodelabで実際に作業するリポジトリ——「Wanstagram」アプリ——を取得します。
以下のコマンドを実行して、GitHubリポジトリをcloneします。

$ git clone https://github.com/outerheavenproject/dagger-codelabs-sample.git

cloneが完了したらAndroid Studioを実行し、cloneしたプロジェクトを指定して開きます。

プロジェクト構成

このプロジェクトではKotlinを採用し、AndroidXに対応済みです。
また、Kotlin Coroutinesを採用していますが、シンプルな記法のみに留めつつ採用しています。
通信ライブラリはRetrofitを採用しています。
JSONシリアライザーとしてkotlinx.serializationを採用しています。
アーキテクチャとしてMVPパターンを採用しています。

なお、./app/src/main/java/com/github/outerheavenproject/wanstagram/のパスは今後<srcBasePath>/と表現します。

このプロジェクトの問題点 と、Daggerで解決できること

このプロジェクトは問題なくアプリが動いていますが、次の問題があります。

1.DogServiceのインスタンスを毎回生成してしまう。

シングルトンに管理したいインスタンスを、自分で生成・管理するのはめんどうです。Daggerを使うことで、安全にシングルトンでインスタンスを管理することが出来ます。

2.DogService RetrofitインターフェースをPresenter内で生成しているので、環境の切り替えが困難

これは、DIパターンを採用することで解決出来ます。Daggerを使うことで、DIパターンをお手軽に導入することが可能になります。

Daggerのインストール

まず、Daggerを導入する最初の第一歩として、GradleにDaggerを設定します。
./app/build.gradledependenciesブロック内に以下のように記述します。
また、kaptを使用するため、kotlin-kaptプラグインを有効にすることを忘れないようにします。

apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-kapt' // 👈
apply plugin: 'kotlinx-serialization'

dependencies {
    // ...
    def dagger_version = '2.23.2' // 👈
    implementation "com.google.dagger:dagger:$dagger_version" // 👈
    kapt "com.google.dagger:dagger-compiler:$dagger_version" // 👈
}

書き換えたら Sync Project with Gradle Filesを実行します。

AppComponentをつくる

まず、Componentアノテーションを使い、AppComponentを定義します。
<srcBasePath>/AppComponent.kt を作成します。

@Singleton
@Component
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(): AppComponent
    }
}

上記のファイルを定義した後、Make Project を実行すると、アノテーションプロセッサーの自動生成により、DaggerAppComponentクラスが生成されます。

次に、<srcBasePath>/App.kt を作成してApplicationクラスを作成し、さきほど生成されたDaggerAppComponentを使います。

class App : Application() {
    lateinit var appComponent: AppComponent

    override fun onCreate() {
        super.onCreate()
        appComponent = DaggerAppComponent.create()
    }
}

Applicationクラスを継承したクラスを作成したので、AndroidManifest.xmlへのApplicationクラスの登録を忘れずに。

<manifest ...>
    <!-- ... -->
    <application
        android:name=".App"
        ...
    />
        <!-- ... -->
    </application>
</manifest>

これで下準備は完了です。

DogServiceインスタンスをDaggerで生成する

現在、DogService.ktに実装されている getDogService() をDaggerから提供するよう書き換えていきます。

<srcBasePath>/DataModule.kt を作成し、以下のように記述します。

@Module
class DataModule {
    @Singleton
    @Provides
    fun provideRetrofit(): Retrofit =
        Retrofit.Builder()
            .baseUrl("https://dog.ceo/api/")
            .addConverterFactory(
                Json.asConverterFactory("application/json".toMediaType())
            )
            .build()

    @Singleton
    @Provides
    fun provideDogService(retrofit: Retrofit): DogService = retrofit.create()
}

これだけだとまだDaggerのComponentとして機能しないので、AppComponentとDataModuleを結びつけます。

<srcBasePath>/AppComponent.kt を再び開き、以下のように書き換えます。

@Singleton
@Component(
    modules = [DataModule::class] // 👈
)
interface AppComponent {
    @Component.Factory
    interface Factory {
        fun create(): AppComponent
    }
}

Daggerから提供されるインスタンスを使用する

先程の実装で、DaggerからRetrofitインスタンスを提供するようになりました。
しかし、まだ実際にRetrofitインスタンスを使用するクラスでDaggerからこれらを受け取る実装ができていません。

getDogService()DogPresenterおよびShibaPresenterで使用されています。
つまりこれらのクラスに対してDaggerから依存関係を注入する必要があります。
どちらもほぼ同じ構成なので、DogPresenterについてのみ解説します。

DogPresenter.kt を以下のように書き換えます。

-class DogPresenter(
-    private val view: DogContract.View
+class DogPresenter @Inject constructor(
+    private val dogService: DogService
 ) : DogContract.Presenter {
+    private lateinit var view: DogContract.View
+
+    fun attachView(view: DogContract.View) {
+        this.view = view
+    }
+
     override suspend fun start() {
-        val dogs = getDogService().getDogs(limit = 20)
+        val dogs = dogService.getDogs(limit = 20)
         withContext(Dispatchers.Main) {
             view.updateDogs(dogs)
         }

また、DogFragment.ktも書き換えます。
DogFragmentにおいて、DogPresenterをDaggerから注入してもらうように書き換えます。

 class DogFragment : Fragment(),
     DogContract.View {
-    private lateinit var presenter: DogContract.Presenter
+    @Inject
+    lateinit var presenter: DogPresenter
+
     private lateinit var dogAdapter: DogAdapter
 
+    override fun onAttach(context: Context) {
+        (activity!!.application as App).appComponent.inject(this) // 👈この時点ではinjectメソッドが存在しません。あとで解決します。
+        super.onAttach(context)
+    }
+
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
         val recycler = view.findViewById<RecyclerView>(R.id.recycler)
         dogAdapter = DogAdapter(navigator = AppNavigatorImpl())
         recycler.layoutManager = GridLayoutManager(context, 2)
         recycler.adapter = dogAdapter
 
-        presenter = DogPresenter(view = this)
+        presenter.attachView(view = this)
 
         lifecycleScope.launch {
             presenter.start()
         }
     }

さて、 appComponent.inject(this)が解決できていないので、AppComponent.ktを書き換えて解決します。

 interface AppComponent {
     @Component.Factory
     interface Factory {
         fun create(): AppComponent
     }
 
+    fun inject(fragment: DogFragment): DogFragment
+    fun inject(fragment: ShibaFragment): ShibaFragment
 }

動かしてみる

Android StudioのMake Project または Command + F9 を実行します。
無事に実行出来たらDaggerの導入はひとまず無事に完了しました!

宿題

diff

ここまでの記事内容の想定回答のdiffです。

Comparing master...intro-dagger · outer-heaven2/dagger-codelabs-sample

テストに使うフレームワーク類の導入

テスト用フレームワーク類を導入します。
AssertionライブラリとしてTruthを使用します。
Android APIを使用するテストは androidTest 内にテストコードを用意して実施しますが、開発マシン上でテストを動作させるほうが高速です。
開発マシン上でAndroid APIをシミュレートするために、Robolectricを導入します。
一部の実装でcoroutineを使用しています。テストコード上では非同期で実行されるコードを待ち合わせる必要がありますが、
それを実現するために kotlinx-coroutines-test を導入します。
Kotlinでの書きやすさを実現するためにいくつか ktx を導入します。

dependencies {
    // ...
    testImplementation 'androidx.test:core-ktx:1.2.0'
    testImplementation 'androidx.test.ext:junit-ktx:1.1.1'
    testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.2.2'
    testImplementation "org.robolectric:robolectric:4.3"
    testImplementation "com.google.truth:truth:0.45"
    // ...
}

テストコードを記述するファイルの作成

前回、Daggerにて依存解決を行ったクラスは DogPresenterShibaPresenter でした。
今回、 DogPresenter のテスト記述について説明します。

Android Studioにおけるテストコードの生成方法について解説します。
まず、テストを作成したいクラスにカーソルのフォーカスを合わせると、💡(電球)アイコンが表示されます。
これをクリックし、Create test を選択します。

Create test

Create testのダイアログでは、初期生成するコードについていくつか確認があります。
今回は画像の通り、 setUp/@Before をONにし、 start メソッドのみチェックを付けて OK を押します。

Create Test dialog

Choose Destination Directory では test ディレクトリを選択します。

Choose Destination Directory dialog

OK を押すと DogPresenterTest が生成され、IDE上で開かれます。

テストを書く

DogPresenterstart()メソッドを実行すると、attachView(view)で設定した DogContract.View に対して取得したデータがセットされます。
つまり、start()メソッドを実行したときにviewの値が変化するかどうかをテストすれば良いことになります。

ひとまず、テストフレームワークを使うためのアノテーションを記述します。

@RunWith(AndroidJUnit4::class) // 👈
class DogPresenterTest {
    // ...
}

次に、(DogPresenterTest.ktの中の)DogPresenterTest クラスの外に以下のモッククラスを記述します。

@RunWith(AndroidJUnit4::class)
class DogPresenterTest {
    // ...
}

// 👇
private class TestDogService : DogService {
    override suspend fun getDog(): Dog {
        return Dog(url = "1", status = "success")
    }

    override suspend fun getDogs(limit: Int): Dogs {
        return Dogs(urls = listOf("1"), status = "success")
    }

    override suspend fun getBleed(bleed: String, limit: Int): Dogs {
        return Dogs(urls = listOf("1"), status = "success")
    }
}

private class TestView : DogContract.View {
    var called: Int = 0

    override fun updateDogs(dogs: Dogs) {
        called += 1
    }
}

DogPresenterTest クラスの内部実装をしていきます。
先程作成したモッククラスを使い、以下のようにテストコードを記述します。

@RunWith(AndroidJUnit4::class)
class DogPresenterTest {
    private lateinit var presenter: DogPresenter
    private lateinit var dogService: DogService
    private lateinit var view: TestView

    @Before
    fun setUp() {
        dogService = TestDogService()
        view = TestView()
        presenter = DogPresenter(dogService = dogService)
        presenter.attachView(view)
    }

    @Test
    fun start() {
        runBlockingTest {
            presenter.start()
        }

        assertThat(view.called).isEqualTo(1)
    }
}

// ...

テストを実行する

手っ取り早い方法としては、DogPresenterTest.ktを開き、コード行数が書かれている箇所にある再生マーク部分をクリックし、Run DogPresenterTestをクリックすることでテストを実行することが出来ます。

Run test

無事にテストをPassできるでしょうか・・・?

宿題

diff

ここまでの記事内容の想定回答のdiffです。

Comparing intro-dagger...intro-dagger-testing · outerheavenproject/dagger-codelabs-sample

Daggerを導入しつつ、本番環境と検証環境を差し替える方法を考えます。

Androidアプリ開発において、本番環境と検証環境を差し替える方法として一般的なのは、
Build variant(Build type (debug,release)やProduct flavor(例としてstaging, production))ごとにフォルダを切り替える機能があります。
Daggerにおいてもこれらの機能を活用するのですが、差し替えたいクラスをvariantごとに提供して直接差し替えるのではなく、Moduleをvariantごとに提供することで間接的に差し替えを行うことが求められます。

というわけで、デバッグ環境ではRetrofitのログ出力を行うが、本番環境ではログ出力をさせない実装を行いたいと思います。

ログ出力用ライブラリの追加

Retrofitの通信ログをLogcatに出力する方法として手っ取り早い方法は、
Retrofitが内部で使用しているOkHttpに対してInterceptorを設定することです。

というわけでlogging-interceptorを依存関係に加えます。

// ...
dependencies {
    // ...
    implementation 'com.squareup.okhttp3:okhttp:4.0.1'
    debugImplementation 'com.squareup.okhttp3:logging-interceptor:4.0.1' // 👈
    // ...
}

Retrofitがカスタムされた OkHttpClient を使用するよう書き換え

RetrofitではOkHttpClientを指定しない場合、デフォルト設定でOkHttpを初期化し、使用します。
その場合ログ出力はされないので、ログ出力をしたい場合はカスタマイズされたOkHttpを使用するようにRetrofitのBuilderで設定します。
以下のように記述することで、カスタマイズされたOkHttpを使います。

肝心のカスタマイズされたOkHttpは次のステップで用意します。

     @Singleton
     @Provides
-    fun provideRetrofit(): Retrofit =
+    fun provideRetrofit(client: OkHttpClient): Retrofit =
         Retrofit.Builder()
             .baseUrl("https://dog.ceo/api/")
             .addConverterFactory(
                 Json.asConverterFactory("application/json".toMediaType())
             )
+            .client(client)
             .build()

debug variant と release variant に OkHttpClient を提供するモジュールを記述

まずは ./app/src/release/java/com/github/outerheavenproject/wanstagram/OkHttpClientModule.kt に以下の内容でOkHttpClientを記述します。

見ればわかる通り、ほぼ何もしていません。

@Module
class OkHttpClientModule {
    @Singleton
    @Provides
    fun provideOkHttpClient(): OkHttpClient =
        OkHttpClient.Builder().build()
}

次に ./app/src/debug/java/com/github/outerheavenproject/wanstagram/OkHttpClientModule.kt を作成して記述します。

御覧の通り、HttpLoggingInterceptorが組み込まれています。

@Module
class OkHttpClientModule {
    @Singleton
    @Provides
    fun provideOkHttpClient(): OkHttpClient =
        OkHttpClient.Builder()
            .addInterceptor(
                HttpLoggingInterceptor().apply {
                    level = HttpLoggingInterceptor.Level.BASIC
                }
            )
            .build()
}

AppComponentにOkHttpClientModuleを登録する

@Componentに記載しないと provideRetrofit(client: OkHttpClient)OkHttpClientが解決できなくなるので、忘れないように記載しましょう。

 @Singleton
 @Component(
-    modules = [DataModule::class]
+    modules = [
+        DataModule::class,
+        OkHttpClientModule::class
+    ]
 )
 interface AppComponent {
     // ...
 }

さて debug バリアントで実際にアプリを動かしてみましょう。
Android StudioのLogcatを確認し、通信のログが表示されたら成功です!

宿題

android {
    // ...
    buildTypes {
        release {
            // ...
            signingConfig signingConfigs.debug
        }
    }

    signingConfigs {
        debug {
            storePassword "android"
            keyAlias "androiddebugkey"
            keyPassword "android"
            storeFile file("${System.getProperty("user.home")}/.android/debug.keystore")
        }
    }
}
// ...

diff

ここまでのdiffは以下のページで確認できます。

Comparing intro-dagger-testing...intro-dagger-build-types · outerheavenproject/dagger-codelabs-sample

これまでの章で、Daggerの基本的な使い方を見てきました。Daggerをより便利に使うために、この章ではSubcomponentと呼ばれている機能について説明しています。

Subcomponent

1つの大きなComponentがあったときに、その大きなComponentを小さいComponent(Subcomponet)に分割することで、依存関係を整理することを可能にする機能です。
また、スコープをそれぞれのSubcomponentで定義することが出来ます(スコープは後の章で説明します)。
これにより、どのContextを使うのか、というAndroid固有の問題を解決することが出来ます。

Subcomponentの定義

まず、MainActivityに対するSubcomponentを定義します。
<srcBasePath>/MainActivitySubcomponent.kt を以下のように実装します。

@Subcomponent(modules = [MainActivityModule::class])
interface MainActivitySubcomponent {
    fun inject(activity: MainActivity): MainActivity
    fun inject(fragment: DogFragment): DogFragment
    fun inject(fragment: ShibaFragment): ShibaFragment

    @Subcomponent.Factory
    interface Factory {
        fun create(
            @BindsInstance context: Context
        ): MainActivitySubcomponent
    }
}

@Module
interface MainActivityModule {
    @Binds
    fun bindAppNavigator(navigator: AppNavigatorImpl): AppNavigator
}
@Module
class MainActivityModule {
    @Provides
    fun bindAppNavigator(navigator: AppNavigatorImpl): AppNavigator {
        return navigator
    }
}

Subcomponentを定義するには、セットで Subcomponent.Factory を定義する必要があります。
@BindsInstanceを使うことで、引数として与えられたインスタンスを用いて、型の解決を試みるようになります。

次に、作成したMainActivitySubcomponentを親のComponentと結びつけます。
あわせて、MainActivitySubcomponentに定義が移動したFragmentのinject定義を削除します。

 @Singleton
 @Component(
     // ...
 )
 interface AppComponent {
     // ...
 
-    fun inject(fragment: DogFragment): DogFragment
-    fun inject(fragment: ShibaFragment): ShibaFragment
+    fun mainActivitySubcomponentFactory(): MainActivitySubcomponent.Factory
 }

これで準備は完了です。

Subcomponentを使う

上記で作成した MainActivitySubcomponent を実際に使ってみます。

class MainActivity : AppCompatActivity() {
    lateinit var subComponent: MainActivitySubcomponent // 👈

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // 👇
        subComponent = (application as App)
            .appComponent
            .mainActivitySubcomponentFactory()
            .create(this)
        subComponent.inject(this)
        // 👆
        
        // ...
    }
}

先程解説をした @BindsInstancemainActivitySubcomponentFactory().create(this) にて与えられます。よって、Subcomponent内で Context が要求されたときは、MainActivityが使われることになります。

次に AppNavigator.kt を以下のように置き換え、Contextの依存解決をするようにします。

 interface AppNavigator {
-    fun navigateToDetail(context: Context, imageUrl: String)
+    fun navigateToDetail(imageUrl: String)
 }

-class AppNavigatorImpl : AppNavigator {
-    override fun navigateToDetail(context: Context, imageUrl: String) {
+class AppNavigatorImpl @Inject constructor(
+    private val context: Context
+) : AppNavigator {
+    override fun navigateToDetail(imageUrl: String) {
         context.startActivity(DetailActivity.createIntent(context, imageUrl))
     }
 }

次に DogAdapter.kt を以下のように置き換えます。

-class DogAdapter(
+class DogAdapter @Inject constructor(
     private val navigator: AppNavigator
 ) : ListAdapter<String, DogViewHolder>(DogDiffUtil) {
     // ...
 
     override fun onBindViewHolder(holder: DogViewHolder, position: Int) {
         // ...
         holder.itemView.setOnClickListener {
-            navigator.navigateToDetail(it.context, dogUrl)
+            navigator.navigateToDetail(dogUrl)
         }
     }
 }

次に DogFragment.kt を以下のように書き換え、DogAdapter の依存解決をします。

 class DogFragment : Fragment(),
     DogContract.View {
     @Inject
     lateinit var presenter: DogPresenter
-    private lateinit var dogAdapter: DogAdapter
+
+    @Inject
+    lateinit var dogAdapter: DogAdapter
 
     override fun onAttach(context: Context) {
-        (activity!!.application as App).appComponent.inject(this)
+        (activity as MainActivity).subComponent.inject(this)
         super.onAttach(context)
     }
 
     // ...
 
     override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
         super.onViewCreated(view, savedInstanceState)
 
         val recycler = view.findViewById<RecyclerView>(R.id.recycler)
-        dogAdapter = DogAdapter(navigator = AppNavigatorImpl())
         recycler.layoutManager = GridLayoutManager(context, 2)
         recycler.adapter = dogAdapter
         // ...
     }
 
     // ...
 }

DogFragment.kt と同じように ShibaFragment.kt も書き換えます。

宿題

AppComponent.kt:

     @Component.Factory
     interface Factory {
-        fun create(): AppComponent
+        fun create(
+            @BindsInstance context: Context
+        ): AppComponent
     }

App.kt:

-        appComponent = DaggerAppComponent.create()
+        appComponent = DaggerAppComponent.factory().create(this)

MainActivitySubcomponent.kt:

     @Subcomponent.Factory
     interface Factory {
         fun create(
-            @BindsInstance context: Context
         ): MainActivitySubcomponent
     }

MainActivity.kt:

         subComponent = (application as App)
             .appComponent
             .mainActivitySubcomponentFactory()
-            .create(this)
+            .create()

diff

ここまでのdiffは以下のページで確認できます。

Comparing intro-dagger-build-types...intro-dagger-subcomponent · outerheavenproject/dagger-codelabs-sample

Dagger.Androidを使用することで、AndroidにおけるDaggerの利便性を高めることが出来ます。
実際に使っていきながら見ていきましょう。

Dagger.Androidのライブラリの追加

Dagger.AndroidはDaggerとは別のライブラリとして提供されています。
./app/build.gradle に以下の依存を追加します。

// ...
dependencies {
    // ...
    implementation "com.google.dagger:dagger-android:$dagger_version"
    implementation "com.google.dagger:dagger-android-support:$dagger_version"
    kapt "com.google.dagger:dagger-android-processor:$dagger_version"
}

HasAndroidInjectorの実装

まずはAppHasAndroidInjector interfaceを実装します。
このinterfaceはAndroidInjectorを提供します。
AndroidInjectorActivityFragmentなどの主要なAndroidの要素について、依存関係の解決を行うためのinterfaceです。

Applicationに対してHasAndroidInjectorを実装することで、ActivityServiceなどの依存関係を解決することが出来ます。なぜApplicationに実装するとActivityの依存関係が解決できるかというと、dagger.android.AndroidInjectionが内部的にActivityなどからApplicationを取得し、injectを行うためです。

class App : Application(), HasAndroidInjector {
    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>

    // ...

    override fun androidInjector(): AndroidInjector<Any> = dispatchingAndroidInjector
}

先程、Activityの依存関係が解決できると書きましたが、ではApplicationの依存関係はどう解決すればよいのでしょうか?
それを次に説明します。

Applicationの依存関係を解決する

さて、Appの依存関係を解決しましょう。
AppComponentAndroidInjector<App>を継承するように変更を加えます。
AppComponent.ktのこれまでの内容を以下に書き換えてしまいます。
Factoryinterfaceの内容を、AndroidInjector.Factoryを使用するようにします。

@Component
interface AppComponent : AndroidInjector<App> {
    @Component.Factory
    interface Factory : AndroidInjector.Factory<App>
}

そして、AndroidInjector#injectAppから呼び出すことでAppの依存関係を解決します。

 class App : Application(), HasAndroidInjector {
     // ...
     override fun onCreate() {
         super.onCreate()
-        appComponent = DaggerAppComponent.create()
+        DaggerAppComponent
+            .factory()
+            .create(this)
+            .inject(this)
     }
     // ...
 }

また、AndroidInjectionModuleAppComponentにインストールしておきましょう。
これはDispatchingAndroidInjectorが依存関係を解決するために必要とするものです。

@Component(
    modules = [
        AndroidInjectionModule::class
    ]
)
interface AppComponent : AndroidInjector<App> {
    // ...
}

Activityの依存関係を解決する

MainActivityの依存関係を解決します。
まずは<srcBasePath>/ui/MainActivityModule.ktを作成します。MainActivity.ktと同じ改装にあるのが望ましいでしょう。

@Module
interface MainActivityModule {
     @ContributesAndroidInjector(
        modules = [
            MainActivityBindModule::class,
        ]
    )
    fun contributeMainActivity(): MainActivity
}

@Module
interface MainActivityBindModule {
    @Binds
    fun bindContext(context: MainActivity): Context

    @Binds
    fun bindAppNavigator(navigator: AppNavigatorImpl): AppNavigator
}

次に、MainActivityModuleAppComponentにインストールします。

直接インストールしても良いのですが、多くのアプリにおいてActivityやその他の依存関係は複雑になっていくため、別途ActivityModuleを用意します。
<srcBasePath>/ActivityModule.ktを作成し、以下の内容で実装します。

@Module(
    includes = [
        MainActivityModule::class
    ]
)
interface ActivityModule

AppComponentには以下のようにActivityModule::classを足します。

@Component(
    modules = [
        AndroidInjectionModule::class,
        ActivityModule::class // 👈
    ]
)
interface AppComponent : AndroidInjector<App> {
    // ...
}

これで準備は整いました。
あとはMainActivityonCreateAndroidInjection#injectを呼び出します。

override fun onCreate(savedInstanceState: Bundle?) {
    AndroidInjection.inject(this)
    super.onCreate(savedInstanceState)
    // subComponent = ... は不要なので削除してよい
    // ...
}

Fragmentの依存関係を解決する

今度はDogFragmentの依存関係を解決しましょう。
まずは先程と同様にDogFragmentModuleを定義します。
<srcBasePath>/ui/dog/DogFragmentModule.kt を作成し、以下のように実装します。
DogFragment.ktと同階層にあるのが望ましいでしょう。

@Module
interface DogFragmentModule {
    @ContributesAndroidInjector
    fun contributeDogFragment(): DogFragment
}

これはMainActivitySubcomponentにインストールします。このSubcomponentはaptにより自動生成されるます。

MainActivityModule.kt に以下のように追記します。

@Module
interface MainActivityModule {
    @ContributesAndroidInjector(
        modules = [
            DogFragmentModule::class // 👈
        ]
    )
    fun contributeMainActivity(): MainActivity
}

Fragmentの依存関係を解決する場合は、そのFragmentがcommitされるActivityにも変更が必要です。
MainActivityにもHasAndroidInjectorを実装しましょう。

class MainActivity : AppCompatActivity(),
    HasAndroidInjector { // 👈 HasAndroidInjectorを実装する

    @Inject
    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
    // 👆

    // ...

    // 👇
    override fun androidInjector(): AndroidInjector<Any> = dispatchingAndroidInjector
}

後はActivityの場合と同じです。
Fragmentの場合はAndroidSupportInjectionを使用します。

 override fun onAttach(context: Context) {
-    (activity as MainActivity).subComponent.inject(this)
+    AndroidSupportInjection.inject(this)
     super.onAttach(context)
 }

もっと簡単に書く

ここまでいろいろと説明してきましたが、HasAndroidInjectorの実装や、injectについてはそれぞれ実装済みの基底クラスが用意されています。

などです。

例えばDaggerApplicationを使うと

class App : DaggerApplication() {
    override fun applicationInjector(): AndroidInjector<out DaggerApplication> =
        DaggerAppComponent.factory().create()
}

このように、多くのボイラープレートコードを削ることが出来ます。
はじめから使ってしまうと何をしているかが分かりづらくなってしまうため、使わない方法を先に紹介しました。
すでに基底クラスがある場合などは、Dagger*を使わない方が良い場合も出てくるでしょう。

例えばDaggerActivityの実装は以下のようになっています。DaggerActivityを用いれば、これらの実装を省略できます。他のクラスに関しても同様です。

/**
 * An {@link Activity} that injects its members in {@link #onCreate(Bundle)} and can be used to
 * inject {@link Fragment}s attached to it.
 */
@Beta
public abstract class DaggerActivity extends Activity implements HasAndroidInjector {

  @Inject DispatchingAndroidInjector<Object> androidInjector;

  @Override
  protected void onCreate(@Nullable Bundle savedInstanceState) {
    AndroidInjection.inject(this);
    super.onCreate(savedInstanceState);
  }

  @Override
  public AndroidInjector<Object> androidInjector() {
    return androidInjector;
  }
}

宿題

diff

ここまでのdiffは以下のページで確認できます。

Comparing intro-dagger-subcomponent...intro-dagger-android-support · outerheavenproject/dagger-codelabs-sample

Scopeを定義し使用することでライフサイクルに沿ったインスタンスを受け取ることが出来ます。
実際にScopeを使用しながら感覚を掴んでいきましょう。

Daggerですでに定義されているScopeとしてSingletonがあります。
これがどのように作用するか見ていきましょう。

Repositoryを作る

今回は、ServicePresenterの間にRepositoryを定義し、Responseを保持して逐一APIにアクセスしなくても良い方法を考えます。
まずはRepositoryを定義しましょう。

class DogRepository @Inject constructor(
    private val dogService: DogService
) {
    companion object {
        private const val LIMIT = 20
    }

    private var dogs: Dogs? = null

    suspend fun findAll(): Dogs {
        dogs?.let { return it }
        return dogService.getDogs(LIMIT)
            .also { dogs = it }
    }
}

Dogsを保持し、あればそちらを返す、なければ新たに取得します。

DogPresenterDogServiceの代わりに使用しましょう。

class DogPresenter @Inject constructor(
    private val repository: DogRepository
) : DogContract.Presenter {

    ...

    override suspend fun start() {
        val dogs = repository.findAll()
        ...
    }
}

この状態でビルドして、「犬」タブから「柴犬」タブに行き、また「犬」タブを開いてみてください。
すると、Repositoryを設定したにもかかわらず、うまく機能していない(毎回APIにアクセスしている)ことが分かるはずです。
これはRepositoryのインスタンスが毎回生成されるため、Dogsの状態を保持できていないからです。

このような場合にScopeが役立ちます。
Singletonを付加してもう一度試してみましょう。

@Singleton // 👈
class DogRepository @Inject constructor(

今度はタブを切り替えても同じ画像が表示されます。
これが基本的なScopeの使い方です。

宿題

今回は宿題ありません😃

diff

ここまでのdiffは以下のページで確認できます。

Comparing intro-dagger-android-support...intro-dagger-scope · outerheavenproject/dagger-codelabs-sample

まずはProvideまたはBindをし忘れているケースです。
troubleshooting-bind-and-provide branchをcheckoutしてbuildしてください。
以下のエラーが出るはずです。

エラー: [Dagger/MissingBinding] com.github.outerheavenproject.wanstagram.ui.dog.DogContract.Presenter cannot be provided without an @Provides-annotated method.
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.github.outerheavenproject.wanstagram.App> {
                ^
      com.github.outerheavenproject.wanstagram.ui.dog.DogContract.Presenter is injected at
          com.github.outerheavenproject.wanstagram.ui.dog.DogFragment.presenter
      com.github.outerheavenproject.wanstagram.ui.dog.DogFragment is injected at
          dagger.android.AndroidInjector.inject(T) [com.github.outerheavenproject.wanstagram.AppComponent → com.github.outerheavenproject.wanstagram.ui.MainActivityModule_ContributeMainActivity.MainActivitySubcomponent → com.github.outerheavenproject.wanstagram.ui.dog.DogFragmentModule_ContributeDogFragment.DogFragmentSubcomponent]

これはDogFragmentDogContract.Presenterを必要としているが、DogContract.Presenterがどこからも供給されておらず、依存が解決できないことを示しています。
この場合、解決方法としてはProvideするかBindするかの大きく2つがあります。今回、DogContract.PresenterDogPresenterで実装されており、bindするだけで良いです。

この依存はDogFragmentで解決できれば良いので、DogFragmentBindModuleを新たに定義し、DogFragmentSubcomponentにinstallします。

@Module
interface DogFragmentModule {
    @ContributesAndroidInjector(
        modules = [
            DogFragmentBindModule::class
        ]
    )
    fun contributeDogFragment(): DogFragment
}

@Module
interface DogFragmentBindModule {
    @Binds
    fun bindPresenter(presenter: DogPresenter): DogContract.Presenter
}

次のケースはScopeがConflictしている場合です。
troubleshooting-conflict-scope branchをcheckoutしてください。

エラー: [com.github.outerheavenproject.wanstagram.ui.dog.DogFragmentModule_ContributeDogFragment.DogFragmentSubcomponent] com.github.outerheavenproject.wanstagram.ui.dog.DogFragmentModule_ContributeDogFragment.DogFragmentSubcomponent has conflicting scopes:
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.github.outerheavenproject.wanstagram.App> {
                ^
    com.github.outerheavenproject.wanstagram.ui.MainActivityModule_ContributeMainActivity.MainActivitySubcomponent also has @com.github.outerheavenproject.wanstagram.di.MyScope

これは親子関係にあるSubcomponentに対して同じScopeを付加しているために発生します。
エラーメッセージにある通り、MainActivitySubcomponentに付加したScopeDogFragmentSubcomponentにも付加されています。

// TODO: MainActivityModuleとMainActivitySubComponentの関係を説明したい

これはどちらかが正しいScopeを使うよう修正すれば解決です。

@Module
interface DogFragmentModule {
    // @MyScope
    @ContributesAndroidInjector
    fun contributeDogFragment(): DogFragment
}

troubleshooting-duplicate branchをcheckoutしてbuildして下さい。
以下のようなエラーが発生します。

エラー: [Dagger/DuplicateBindings] com.github.outerheavenproject.wanstagram.ui.AppNavigator is bound multiple times:
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.github.outerheavenproject.wanstagram.App> {
                ^
      @org.jetbrains.annotations.NotNull @Binds com.github.outerheavenproject.wanstagram.ui.AppNavigator com.github.outerheavenproject.wanstagram.ui.MainActivityBindModule.bindAppNavigator(com.github.outerheavenproject.wanstagram.ui.AppNavigatorImpl)
      @org.jetbrains.annotations.NotNull @Binds com.github.outerheavenproject.wanstagram.ui.AppNavigator com.github.outerheavenproject.wanstagram.ui.dog.DogFragmentBindModule.bindAppNavigator(com.github.outerheavenproject.wanstagram.ui.AppNavigatorImpl)
      com.github.outerheavenproject.wanstagram.ui.AppNavigator is injected at
          com.github.outerheavenproject.wanstagram.ui.DogAdapter(..., navigator)
      com.github.outerheavenproject.wanstagram.ui.DogAdapter is injected at
          com.github.outerheavenproject.wanstagram.ui.dog.DogFragment.dogAdapter
      com.github.outerheavenproject.wanstagram.ui.dog.DogFragment is injected at
          dagger.android.AndroidInjector.inject(T) [com.github.outerheavenproject.wanstagram.AppComponent → com.github.outerheavenproject.wanstagram.ui.MainActivityModule_ContributeMainActivity.MainActivitySubcomponent → com.github.outerheavenproject.wanstagram.ui.dog.DogFragmentModule_ContributeDogFragment.DogFragmentSubcomponent]

これは定義が重複している場合に発生します。
1人のプロジェクトや小規模なプロジェクトではあまり起きませんが、大規模なプロジェクトでグラフを完全に把握出来ていないような場合にしばしば発生します。

今回の場合はAppNavigatorの定義がMainActivityModuleDogFragmentModuleで重複しています。一方の不要な定義を消すことで解消します。

@Module
interface DogFragmentModule {
    @ContributesAndroidInjector
    fun contributeDogFragment(): DogFragment
}

次はtroubleshooting-recursion branchをcheckoutしbuildして下さい。

e: [kapt] An exception occurred: java.lang.StackOverflowError

これはModule定義などがループしている場合に起きるエラーです。発生した場合は再帰的な参照が発生している箇所を探す必要があります。

この場合はDogFragmentModuleの定義がループしていますので、これを修正すれば良いです。

@Module
interface DogFragmentModule {
    @ContributesAndroidInjector
    fun contributeDogFragment(): DogFragment
}

ここからはScopeの章から続いて、実際に自分で定義したScopeを使いながら、より実践的な使い方を紹介します。
Custom Scopeについて解説する前に、既存のWanstagramアプリに機能強化をします(そうすることでCustom Scopeの良さが分かりやすくなります)。

今回はWanstagramに、複数の写真を選択してシェアする機能を作りましょう。
おおまかな仕様としては以下のようになります:

intro-dagger-scope のbranchから実装をはじめ、上記を満たす機能を開発します。

ちなみに依存関係は以下のようになります。

image

画像リソース, XMLの追加

これから使用する画像リソースやXMLファイルについては本筋とは関係がないので、以下のコミットからそのままプロジェクトに取り込んで使用してください。

Add resources · outerheavenproject/dagger-codelabs-sample@dd3cbb2

DogActionBottomSheet の実装

DogActionBottomSheet を実装します。

新しい画面実装をしますが、ここまではこれまで学習したことを使っているだけです。落ち着いて実装しましょう。

<srcBasePath>/ui/dogaction/DogActionBottomSheetContract.kt:

interface DogActionBottomSheetContract {
    interface View {
    }

    interface Presenter {
        fun start(url: String)
        fun share()
    }
}

<srcBasePath>/ui/dogaction/DogActionBottomSheetDialogFragment.kt:

class DogActionBottomSheetDialogFragment : BottomSheetDialogFragment(),
    DogActionBottomSheetContract.View {
    companion object {
        const val TAG = "DogActionBottomSheetDialogFragment"
        private const val URL_KEY = "url"

        fun newInstance(url: String) =
            DogActionBottomSheetDialogFragment()
                .apply { arguments = bundleOf(URL_KEY to url) }
    }

    @Inject
    lateinit var presenter: DogActionBottomSheetContract.Presenter

    private val url: String by lazy {
        requireArguments().getString(URL_KEY) ?: throw IllegalStateException()
    }

    override fun onAttach(context: Context) {
        AndroidSupportInjection.inject(this)
        super.onAttach(context)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View =
        LayoutInflater.from(requireContext())
            .inflate(
                R.layout.dog_action_bottom_sheet_dialog_fragment,
                container,
                false
            )
            .also {
                it.setOnClickListener {
                    presenter.share()
                    dismiss()
                }
            }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        presenter.start(url)
    }
}

<srcBasePath>/ui/dogaction/DogActionBottomSheetPresenter.kt:

class DogActionBottomSheetPresenter @Inject constructor(
    private val sink: DogActionSink,
    private val view: DogActionBottomSheetContract.View
) : DogActionBottomSheetContract.Presenter {
    private lateinit var url: String

    override fun start(url: String) {
        this.url = url
    }

    override fun share() {
        sink.write(url)
    }
}

<srcBasePath>/ui/dogaction/DogActionSink.kt:

interface DogActionSink {
    fun write(url: String)
}

<srcBasePath>/ui/dogaction/DogActionBottomSheetDialogFragmentModule.kt:

@Module
interface DogActionBottomSheetDialogFragmentModule {
    @FragmentScope
    @ContributesAndroidInjector(
        modules = [
            DogActionBottomSheetDialogFragmentBindModule::class
        ]
    )
    fun contributeDogActionBottomSheetDialogFragment(): DogActionBottomSheetDialogFragment
}

@Module
interface DogActionBottomSheetDialogFragmentBindModule {
    @Binds
    fun bindView(fragment: DogActionBottomSheetDialogFragment): DogActionBottomSheetContract.View

    @Binds
    fun bindPresenter(presenter: DogActionBottomSheetPresenter): DogActionBottomSheetContract.Presenter
}

Fragmentに HasAndroidInjector を実装する

DogFragmentShibaFragmentHasAndroidInjectorを実装します(いつものとおりDogFragmentのdiffのみですが、ShibaFragmentも同じく対応します)。

-class DogFragment : Fragment(), DogContract.View {
+class DogFragment : Fragment(), DogContract.View, HasAndroidInjector {
+    @Inject
+    lateinit var dispatchingAndroidInjector: DispatchingAndroidInjector<Any>
+    
 // ...
+
+    override fun androidInjector(): AndroidInjector<Any> = dispatchingAndroidInjector
 }

AppNavigatorの拡張

それぞれのFragmentで発生したイベントを実行する AppNavigator を拡張して、BottomSheetを開くなどのイベントに対応させます。

 interface AppNavigator {
     fun navigateToDetail(imageUrl: String)
+    fun navigateToAction(childFragmentManager: FragmentManager, url: String)
+    fun shareUris(context: Context, uris: ArrayList<Uri>)
 }

 class AppNavigatorImpl @Inject constructor(
     private val context: Context
 ) : AppNavigator {
     // ...
+
+    override fun navigateToAction(childFragmentManager: FragmentManager,  url: String) {
+        DogActionBottomSheetDialogFragment.newInstance(url)
+            .show(childFragmentManager,  DogActionBottomSheetDialogFragment.TAG)
+    }
+
+    override fun shareUris(context: Context, uris: ArrayList<Uri>) {
+        context.startActivity(
+            Intent.createChooser(
+                Intent().apply {
+                    action = Intent.ACTION_SEND_MULTIPLE
+                    type = "image/*"
+                    putParcelableArrayListExtra(Intent.EXTRA_STREAM, uris)
+                },
+                ""
+            )
+        )
+    }
 }

そして、 DogAdapter を書き換え、追加したイベントをコールします。

 class DogAdapter @Inject constructor(
+    private val childFragmentManager: FragmentManager,
     private val navigator: AppNavigator
 ) : ListAdapter<String, DogViewHolder>(DogDiffUtil) {
     // ...
     override fun onBindViewHolder(holder: DogViewHolder, position: Int) {
         // ...
+        holder.itemView.setOnLongClickListener {
+            navigator.navigateToAction(childFragmentManager, dogUrl)
+            true
+        }
     }
 }

MainPresenterの実装

MainActivityですべきことが増えたため、MainPresenterを実装します。

<srcBasePath>/ui/MainContract.kt:

interface MainContract {
    interface View {
        fun shareDogs(dogs: Set<String>)
    }

    interface Presenter {
        fun start()
        fun share()
    }
}

<srcBasePath>/ui/MainPresenter.kt:

class MainPresenter @Inject constructor(
    private val view: MainContract.View
) : MainContract.Presenter, DogActionSink {
    private val shareList = mutableSetOf<String>()

    override fun start() {
    }

    override fun write(url: String) {
        shareList.add(url)
    }

    override fun share() {
        view.shareDogs(shareList)
    }
}

MainActivityで新たに実装したPresenterに対応させます。

-class MainActivity : AppCompatActivity(), HasAndroidInjector {
+class MainActivity : AppCompatActivity(), HasAndroidInjector, MainContract.View {
+    @Inject
+    lateinit var presenter: MainContract.Presenter
+
+    @Inject
+    lateinit var navigator: AppNavigator
 
     // ...
 
     override fun onCreate(savedInstanceState: Bundle?) {
         // ...
+        findViewById<View>(R.id.fab)
+            .setOnClickListener { presenter.share() }
+
+        presenter.start()
     }
 
+    override fun shareDogs(dogs: Set<String>) {
+        navigator.shareUris(this, ArrayList(dogs.map { it.toUri() }))
+    }
 
     // ...
 }

MainActivityModuleでModuleのインストール, 追加

新しく作成した画面や、拡張したクラスで依存関係が増えたので、対応させます。
MainActivityModuleを編集し、Moduleのインストールや依存関係の追加をします。

 @Module
 interface MainActivityModule {
     @ActivityScope
     @ContributesAndroidInjector(
         modules = [
+            MainActivityProvidesModule::class,
             MainActivityBindModule::class,
             DogFragmentModule::class,
-            ShibaFragmentModule::class
+            ShibaFragmentModule::class,
+            DogActionBottomSheetDialogFragmentModule::class
         ]
     )
     fun contributeMainActivity(): MainActivity
 }
 
+@Module
+class MainActivityProvidesModule {
+    @Provides
+    fun provideFragmentManager(activity: MainActivity): FragmentManager {
+        return activity.supportFragmentManager
+    }
+}
+
 @Module
 interface MainActivityBindModule {
     @Binds
     fun bindContext(context: MainActivity): Context
 
     @Binds
     fun bindAppNavigator(navigator: AppNavigatorImpl): AppNavigator
+
+    @Binds
+    fun bindView(activity: MainActivity): MainContract.View
+
+    @Binds
+    fun bindPresenter(presenter: MainPresenter): MainContract.Presenter
+
+    @Binds
+    fun bindDogActionSink(presenter: MainPresenter): DogActionSink
 }

ここまでが準備編です。
各パーツは揃っているのでコンパイルは通りますが、実際に動かしてみると想定通りに動きません。いくら画像を長押しして「後でシェアする」を押し、FABを押しても空のリストが帰ってきます。

それは何故でしょうか?それはどうやったら修正できるでしょうか?

続く・・・。

先程の章ではコードレベルでは問題のない実装を行いましたが、実際はうまく動きませんでした。
その理由を後ほど説明しつつ、Custom Scopeを実装していきます。

Custom Scopeの作成

この章で最重要の Scope アノテーションを実装しますが、内容は極めてシンプルです。

今回はActivityのためのScopeとしてActivityScopeFragmentのためのScopeとしてFragmentScopeを定義します。

<srcBasePath>/di/ActivityScope.kt を以下の内容で実装します。

@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class ActivityScope

<srcBasePath>/di/FragmentScope.kt を以下の内容で実装します。

@Scope
@MustBeDocumented
@Retention(AnnotationRetention.RUNTIME)
annotation class FragmentScope

Custom Scopeアノテーションの付与

MainActivityのSubComponentにActivityScopeを付加します。

@Module
interface MainActivityModule {
    @ActivityScope // 👈
    @ContributesAndroidInjector(...)
    fun contributeMainActivity(): MainActivity
}

続いてDogActionBottomSheetDialogFragmentにはFragmentScopeを付加します。

@Module
interface DogActionBottomSheetDialogFragmentModule {
    @FragmentScope // 👈
    @ContributesAndroidInjector
    fun contributeDogActionBottomSheetDialogFragment(): DogActionBottomSheetDialogFragment
}

MainPresenter / DogActionSink

image

先程のクラス図を見るとDogActionSinkというinterfaceがあることに気づくでしょう。
今回はこのinterfaceのwriteを呼び出すことで、シェアリストへの追加を実現します。
このDogActionSinkの実体はMainPresenterです。

ここで考えるべきこととして、今の状態ではMainPresenterのインスタンスを毎回生成するため、DogActionSinkをいくら呼び出したとしても、MainActivityから見えるシェアリストは空であるということです。
MainActivityから参照されるMainContract$PresenterDogActionBottomSheetPresenterから参照されるDogActionSink、これらはすべて同じインスタンスである必要があります。 (混乱するかもしれませんが、MainContract$PresenterDogActionSinkの実体は同じMainPresenterです。)

この課題を解決できるのがScopeです。
まずはMainPresenterActivityScope付加せずに アプリの動作を試してみてください。

class MainPresenter @Inject constructor(
    private val view: MainContract.View
) : MainContract.Presenter, DogActionSink {

MainPresenter#writeあたりにbreakpointを置いて確認してみると、Bottom sheetから参照されるMainPresenterが毎回生成されていることが分かるでしょう。

それではMainPresenterActivityScopeを付加してみましょう。

@ActivityScope
class MainPresenter @Inject constructor(
    private val view: MainContract.View
) : MainContract.Presenter, DogActionSink {

今度はインスタンスが保持され、期待した挙動になっていることが確認できます。

まとめ

このチャプターではCustom Scopeの使い方について実際に挙動を見ながら確認していきました。

Scopeの章と見比べると、実際にインスタンス管理をしたいクラスに加えてSubcomponentに対して同じアノテーションを付与するということを理解すれば、実装すること自体は簡単だということが分かります。

最近ではMVVMを採用する場合にはandroidx.lifecycle.ViewModelProviderもあるためScopeが必要な機会はかなり少なくなってきているかとは思いますが、Fluxなどを採用する場合には有効な知識です。

diff

ここまでのdiffは以下のページで確認できます。

Comparing intro-dagger-scope...intro-dagger-custom-scope · outerheavenproject/dagger-codelabs-sample

次のケースはScopeをつけ間違えている場合です。
troubleshooting-different-scope branchをcheckoutしてください。

エラー: [Dagger/IncompatiblyScopedBindings] com.github.outerheavenproject.wanstagram.ui.MainActivityModule_ContributeMainActivity.MainActivitySubcomponent scoped with @com.github.outerheavenproject.wanstagram.di.ActivityScope may not reference bindings with different scopes:
public abstract interface AppComponent extends dagger.android.AndroidInjector<com.github.outerheavenproject.wanstagram.App> {
                ^
      @com.github.outerheavenproject.wanstagram.di.FragmentScope class com.github.outerheavenproject.wanstagram.ui.MainPresenter [com.github.outerheavenproject.wanstagram.AppComponent → com.github.outerheavenproject.wanstagram.ui.MainActivityModule_ContributeMainActivity.MainActivitySubcomponent]

これはScopeのライフサイクルを間違えてInjectしている場合に起きます。例えば、Fragmentのライフサイクルに応じたScopeを付加しているクラスをActivityに対してInjectしようとしている場合などです。FragmentSubcomponentActivitySubcomponentの子として定義している場合には、この関係は成立しないため、エラーとなります。
また、親子関係にない場合でもそれぞれに付加しているScopeを間違えて使用した場合には起きます。

この修正方法としては、Scopeを正しくつけ直すことが必要となります。
エラーメッセージを見て下さい。
MainActivitySubcomponentActivityScopeが付加されているが、MainPresenterにはFragmentScopeが付加されていると書いてあります。
今回の場合、MainPresenterMainActivityで使用されるため、ActivityScopeが妥当です。そのように修正すれば、このエラーは解消できます。

@ActivityScope
class MainPresenter @Inject constructor(
    private val view: MainContract.View
) : MainContract.Presenter, DogActionSink {