大規模なAndroidアプリではDI(Dependency Injection; 依存性の注入)ライブラリが組み込まれていることがあります。
多重に依存関係が存在するクラス群の初期化をDIライブラリに担わせることにより、開発者はより本質的なコード実装に時間を使うことが出来ます。
Daggerは、Androidアプリ開発におけるDIライブラリとしてよく採用されているライブラリです。
コードのコンパイル時にDIコンテナを自動生成するため、実行時において速いことが謳われています。
現在はGoogleによってメンテナンスされています。
Daggerを導入することにより、以下の点においてメリットがあります。
このCodelabsでは、上記で述べたDaggerの恩恵について、実際にコードを書いて体感していく形式をとります。
具体的には以下の内容を収録しています。
このCodelabsは、Daggerをこれから知りたいと思っているAndroid開発者に向けて制作されたものです。
特に、なんとなくでDaggerを使っている(使うことを強いられている)開発者が自分の力でDaggerを導入できるようになるよう配慮を心がけました。
そのため、このCodelabsでは、Daggerがどういう根拠で動いているのか、どう便利なのか、どのようなときにDaggerを導入するべきなのか、といった文章は一切ありません。
まず動作するコードを自力で書いてから、理屈や理論は(他の資料を見て)あとで身につけてもらうというアプローチを採用しました。
本Codelabsは、まず「Daggerが導入されていないアプリをDaggerに対応させる」というロールプレイ方式で、Daggerの良さを手を動かしながら学びます。
後半ではDaggerで起こりがちなトラブルとその対応例を紹介します。
まず序盤から手を動かしてみることで、Daggerに対する「よく分からない感」を払拭できればいいなと思います。
そして、後半を進めていくことで、Daggerをより使いこなし、日常業務でも複雑な処理を苦もなく実現できるようになれば、制作者冥利に尽きます。
さぁ、始めましょう!
まずは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>/
と表現します。
このプロジェクトは問題なくアプリが動いていますが、次の問題があります。
1.DogServiceのインスタンスを毎回生成してしまう。
シングルトンに管理したいインスタンスを、自分で生成・管理するのはめんどうです。Daggerを使うことで、安全にシングルトンでインスタンスを管理することが出来ます。
2.DogService RetrofitインターフェースをPresenter内で生成しているので、環境の切り替えが困難
これは、DIパターンを採用することで解決出来ます。Daggerを使うことで、DIパターンをお手軽に導入することが可能になります。
まず、Daggerを導入する最初の第一歩として、GradleにDaggerを設定します。./app/build.gradle
の dependencies
ブロック内に以下のように記述します。
また、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
を実行します。
まず、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.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から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の導入はひとまず無事に完了しました!
ShibaPresenter
もDogService
をDaggerで注入するよう書き換えてみましょう。DogService.kt
のgetDogService()
を削除しても動作するはずです。やってみよう。DogService
のインスタンスがどう変化しているか確認してみよう。(例えば dogService.hashCode()
はインスタンスが同じかどうかを調べるには良い方法です)ここまでの記事内容の想定回答の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にて依存解決を行ったクラスは DogPresenter
と ShibaPresenter
でした。
今回、 DogPresenter
のテスト記述について説明します。
Android Studioにおけるテストコードの生成方法について解説します。
まず、テストを作成したいクラスにカーソルのフォーカスを合わせると、💡(電球)アイコンが表示されます。
これをクリックし、Create test
を選択します。
Create test
のダイアログでは、初期生成するコードについていくつか確認があります。
今回は画像の通り、 setUp/@Before
をONにし、 start
メソッドのみチェックを付けて OK
を押します。
Choose Destination Directory
では test
ディレクトリを選択します。
OK
を押すと DogPresenterTest
が生成され、IDE上で開かれます。
DogPresenter
のstart()
メソッドを実行すると、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
をクリックすることでテストを実行することが出来ます。
無事にテストをPassできるでしょうか・・・?
isEqualTo(1)
を isNotEqualTo(1)
とか isEqualTo(0)
に変えてテストを実行してみて、テストが落ちることを確認しましょう。ShibaPresenter
のテストはまだ書かれていません。書いてみましょう。ここまでの記事内容の想定回答の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を指定しない場合、デフォルト設定で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()
まずは ./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()
}
@Component
に記載しないと provideRetrofit(client: OkHttpClient)
の OkHttpClient
が解決できなくなるので、忘れないように記載しましょう。
@Singleton
@Component(
- modules = [DataModule::class]
+ modules = [
+ DataModule::class,
+ OkHttpClientModule::class
+ ]
)
interface AppComponent {
// ...
}
さて debug
バリアントで実際にアプリを動かしてみましょう。
Android StudioのLogcatを確認し、通信のログが表示されたら成功です!
release
バリアントではログが出ないことを確認しましょう。./app/build.gradle
を以下のように設定します。android {
// ...
buildTypes {
release {
// ...
signingConfig signingConfigs.debug
}
}
signingConfigs {
debug {
storePassword "android"
keyAlias "androiddebugkey"
keyPassword "android"
storeFile file("${System.getProperty("user.home")}/.android/debug.keystore")
}
}
}
// ...
production
フレーバーと staging
フレーバーを用意します。先程リンクを紹介したビルドバリアントの設定を参考にしてください。ここまでのdiffは以下のページで確認できます。
これまでの章で、Daggerの基本的な使い方を見てきました。Daggerをより便利に使うために、この章ではSubcomponentと呼ばれている機能について説明しています。
1つの大きなComponentがあったときに、その大きなComponentを小さいComponent(Subcomponet)に分割することで、依存関係を整理することを可能にする機能です。
また、スコープをそれぞれのSubcomponentで定義することが出来ます(スコープは後の章で説明します)。
これにより、どのContext
を使うのか、というAndroid固有の問題を解決することが出来ます。
まず、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
}
これで準備は完了です。
上記で作成した 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)
// 👆
// ...
}
}
先程解説をした @BindsInstance
は mainActivitySubcomponentFactory().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
も書き換えます。
Context
は一致しているのでコンパイルも通り、一見アプリも動きますが、サムネイルをタップするとクラッシュします。理由はLogcatをみると分かります。確認してみましょう。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()
MainActivitySubcomponent.kt
, MainActivity.kt
の修正を戻すとこの状態を再現させることが出来ます。しかし、Makeするとエラー: [Dagger/DuplicateBindings] android.content.Context is bo
und multiple times が発生します。AppComponent
側の @BindsInstance
で Context
ではなく Application
と定義することですここまでのdiffは以下のページで確認できます。
Dagger.Androidを使用することで、AndroidにおけるDaggerの利便性を高めることが出来ます。
実際に使っていきながら見ていきましょう。
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
の実装まずはApp
でHasAndroidInjector
interfaceを実装します。
このinterfaceはAndroidInjector
を提供します。AndroidInjector
はActivity
、Fragment
などの主要なAndroidの要素について、依存関係の解決を行うためのinterfaceです。
Application
に対してHasAndroidInjector
を実装することで、Activity
やService
などの依存関係を解決することが出来ます。なぜ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
の依存関係はどう解決すればよいのでしょうか?
それを次に説明します。
さて、App
の依存関係を解決しましょう。AppComponent
がAndroidInjector<App>
を継承するように変更を加えます。AppComponent.kt
のこれまでの内容を以下に書き換えてしまいます。Factory
interfaceの内容を、AndroidInjector.Factory
を使用するようにします。
@Component
interface AppComponent : AndroidInjector<App> {
@Component.Factory
interface Factory : AndroidInjector.Factory<App>
}
そして、AndroidInjector#inject
をApp
から呼び出すことでApp
の依存関係を解決します。
class App : Application(), HasAndroidInjector {
// ...
override fun onCreate() {
super.onCreate()
- appComponent = DaggerAppComponent.create()
+ DaggerAppComponent
+ .factory()
+ .create(this)
+ .inject(this)
}
// ...
}
また、AndroidInjectionModule
をAppComponent
にインストールしておきましょう。
これはDispatchingAndroidInjector
が依存関係を解決するために必要とするものです。
@Component(
modules = [
AndroidInjectionModule::class
]
)
interface AppComponent : AndroidInjector<App> {
// ...
}
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
}
次に、MainActivityModule
をAppComponent
にインストールします。
直接インストールしても良いのですが、多くのアプリにおいてActivity
やその他の依存関係は複雑になっていくため、別途ActivityModule
を用意します。<srcBasePath>/ActivityModule.kt
を作成し、以下の内容で実装します。
@Module(
includes = [
MainActivityModule::class
]
)
interface ActivityModule
AppComponent
には以下のようにActivityModule::class
を足します。
@Component(
modules = [
AndroidInjectionModule::class,
ActivityModule::class // 👈
]
)
interface AppComponent : AndroidInjector<App> {
// ...
}
これで準備は整いました。
あとはMainActivity
のonCreate
でAndroidInjection#inject
を呼び出します。
override fun onCreate(savedInstanceState: Bundle?) {
AndroidInjection.inject(this)
super.onCreate(savedInstanceState)
// subComponent = ... は不要なので削除してよい
// ...
}
今度は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
DaggerAppCompatActivity
DaggerFragment
などです。
例えば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;
}
}
DaggerApplication
, DaggerAppCompatActivity
などを用いたやり方を試してみよう。(なお、今後出てくるコードではDaggerApplication
などを使う方法ではなく、今回解説したコードベースで説明が続きます。。。)ここまでのdiffは以下のページで確認できます。
Scope
を定義し使用することでライフサイクルに沿ったインスタンスを受け取ることが出来ます。
実際にScope
を使用しながら感覚を掴んでいきましょう。
Daggerですでに定義されているScope
としてSingleton
があります。
これがどのように作用するか見ていきましょう。
今回は、Service
とPresenter
の間に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
を保持し、あればそちらを返す、なければ新たに取得します。
DogPresenter
でDogService
の代わりに使用しましょう。
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は以下のページで確認できます。
まずは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]
これはDogFragment
がDogContract.Presenter
を必要としているが、DogContract.Presenter
がどこからも供給されておらず、依存が解決できないことを示しています。
この場合、解決方法としてはProvide
するかBind
するかの大きく2つがあります。今回、DogContract.Presenter
はDogPresenter
で実装されており、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
に付加したScope
がDogFragmentSubcomponent
にも付加されています。
// 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
の定義がMainActivityModule
とDogFragmentModule
で重複しています。一方の不要な定義を消すことで解消します。
@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から実装をはじめ、上記を満たす機能を開発します。
ちなみに依存関係は以下のようになります。
これから使用する画像リソースや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
}
HasAndroidInjector
を実装するDogFragment
とShibaFragment
にHasAndroidInjector
を実装します(いつものとおり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
}
それぞれの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
+ }
}
}
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のインストールや依存関係の追加をします。
@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を実装していきます。
この章で最重要の Scope
アノテーションを実装しますが、内容は極めてシンプルです。
今回はActivity
のためのScope
としてActivityScope
、Fragment
のための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
MainActivity
のSubComponentにActivityScope
を付加します。
@Module
interface MainActivityModule {
@ActivityScope // 👈
@ContributesAndroidInjector(...)
fun contributeMainActivity(): MainActivity
}
続いてDogActionBottomSheetDialogFragment
にはFragmentScope
を付加します。
@Module
interface DogActionBottomSheetDialogFragmentModule {
@FragmentScope // 👈
@ContributesAndroidInjector
fun contributeDogActionBottomSheetDialogFragment(): DogActionBottomSheetDialogFragment
}
MainPresenter
/ DogActionSink
先程のクラス図を見るとDogActionSink
というinterfaceがあることに気づくでしょう。
今回はこのinterfaceのwrite
を呼び出すことで、シェアリストへの追加を実現します。
このDogActionSink
の実体はMainPresenter
です。
ここで考えるべきこととして、今の状態ではMainPresenter
のインスタンスを毎回生成するため、DogActionSink
をいくら呼び出したとしても、MainActivity
から見えるシェアリストは空であるということです。MainActivity
から参照されるMainContract$Presenter
、DogActionBottomSheetPresenter
から参照されるDogActionSink
、これらはすべて同じインスタンスである必要があります。 (混乱するかもしれませんが、MainContract$Presenter
とDogActionSink
の実体は同じMainPresenter
です。)
この課題を解決できるのがScope
です。
まずはMainPresenter
にActivityScope
を 付加せずに アプリの動作を試してみてください。
class MainPresenter @Inject constructor(
private val view: MainContract.View
) : MainContract.Presenter, DogActionSink {
MainPresenter#write
あたりにbreakpointを置いて確認してみると、Bottom sheetから参照されるMainPresenter
が毎回生成されていることが分かるでしょう。
それではMainPresenter
にActivityScope
を付加してみましょう。
@ActivityScope
class MainPresenter @Inject constructor(
private val view: MainContract.View
) : MainContract.Presenter, DogActionSink {
今度はインスタンスが保持され、期待した挙動になっていることが確認できます。
このチャプターではCustom Scope
の使い方について実際に挙動を見ながら確認していきました。
Scopeの章と見比べると、実際にインスタンス管理をしたいクラスに加えてSubcomponentに対して同じアノテーションを付与するということを理解すれば、実装すること自体は簡単だということが分かります。
最近ではMVVMを採用する場合にはandroidx.lifecycle.ViewModelProvider
もあるためScope
が必要な機会はかなり少なくなってきているかとは思いますが、Fluxなどを採用する場合には有効な知識です。
ここまでの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
しようとしている場合などです。Fragment
のSubcomponent
をActivity
のSubcomponent
の子として定義している場合には、この関係は成立しないため、エラーとなります。
また、親子関係にない場合でもそれぞれに付加しているScope
を間違えて使用した場合には起きます。
この修正方法としては、Scope
を正しくつけ直すことが必要となります。
エラーメッセージを見て下さい。MainActivitySubcomponent
はActivityScope
が付加されているが、MainPresenter
にはFragmentScope
が付加されていると書いてあります。
今回の場合、MainPresenter
はMainActivity
で使用されるため、ActivityScope
が妥当です。そのように修正すれば、このエラーは解消できます。
@ActivityScope
class MainPresenter @Inject constructor(
private val view: MainContract.View
) : MainContract.Presenter, DogActionSink {