Dagger-AndroidでUserScopeのようなカスタムのScopeを使い、特定のActivity間のみで同一インスタンスを使う方法

Daggerを使い、複数のインスタンス間で同一のインスタンスを使う時は、スコープを使うことで実現できます。 Androidでは、すべてのActivityで共通のインスタンスを使うには SingletonスコープとAppComponentを組み合わせて使う方法がよく知られています。 しかし、__特定__のActivity間でのみ共通のインスタンスを使いたい場合にはこの方法は使えません。Singletonだと__すべて__のActivity間で共通のインスタンスが使えてしまいます。

この記事では、Dagger-Androidを使ったサンプルコードをベースに、「特定のActivity間のみで同一インスタンスを使う方法」を説明します。 また、基本的なDaggerの使い方は知っている前提で説明していきます。

サンプルコードはこちらになります。 コードを見ると理解がより深まると思うので、ぜひご覧になってください😊


では説明していきます。今回のサンプルコードの目指すところは

とします。

まず最初にUserScopeを定義します。

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

次にUserSubcomponentを作ります。

@UserScope
@Subcomponent
interface UserSubcomponent {
  @Subcomponent.Builder
  interface Builder {
    fun build(): UserSubcomponent
  }

  val activityInjector: DispatchingAndroidInjector<Activity>
}

ここでは、UserSubcomponentにUserScopeスコープを持たせています。 このように書くことで、SubcomponentとScopeを結びつけることが出来ます。

次にAppComponentを作ります。

@Singleton
@Component(
    modules = [
      AndroidSupportInjectionModule::class
    ]
)
interface AppComponent : AndroidInjector<App> {
  @Component.Builder
  interface Builder {
    @BindsInstance
    fun application(application: App): Builder

    fun build(): AppComponent
  }

  override fun inject(app: App)

  // AppComponentとUserSubcomponentを結びつける
  val userComponentBuilder: UserSubcomponent.Builder
}

ここでのポイントはAppComponentに、val userComponentBuilder: UserSubcomponent.Builderを定義することです。 こうすることで、AppComponentUserSubcomponentを結び付けることが出来ます。

これで基本的な部分の定義は完了しました。


次に、各ActivityをComponentに結びつけていきます。 サンプルではMainActivity、UserScopedActivityとNoUserScopedActivityの3つのActivityを定義しており、 それぞれのActivityは以下のように振る舞わせたいとします。

MainActivity、UserScopedActivityをUserSubcomponentに従わせる

MainActivity、UserScopedActivityをUserSubcomponentに定義することで、MainActivity、UserScopedActivityをUserScopeに従わせることが出来ます。 なぜなら、UserSubcomponentUserScopeに紐付いているためです。

// UserSubcomponent.kt
@UserScope
@Subcomponent(modules = [
  MainActivityModule::class,
  UserScopedActivityModule::class
])
interface UserSubcomponent {
  @Subcomponent.Builder
  interface Builder {
    fun build(): UserSubcomponent
  }

  val activityInjector: DispatchingAndroidInjector<Activity>
}

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

// UserScopedActivityModule.kt
@Module
interface UserScopedActivityModule {
  @ContributesAndroidInjector
  fun contributeUserScopedActivity(): UserScopedActivity
}

NoUserScopedActivityはUserScopeに従わない

NoUserScopedActivityをAppComponentに定義することで、NoUserScopedActivityでUserScopeを使っていたらコンパイルエラーにすることが出来ます。 AppComponentUserScopeに紐付いていないためです。

// AppComponent.kt
@Singleton
@Component(
    modules = [
      AndroidSupportInjectionModule::class,
      NoUserScopedActivityModule::class
    ]
)
interface AppComponent : AndroidInjector<App> {
  @Component.Builder
  interface Builder {
    @BindsInstance
    fun application(application: App): Builder

    fun build(): AppComponent
  }

  override fun inject(app: App)

  val userComponentBuilder: UserSubcomponent.Builder
}

// NoUserScopedActivityModule.kt
@Module
interface NoUserScopedActivityModule {
  @ContributesAndroidInjector
  fun contributeNoUserScopedActivity(): NoUserScopedActivity
}

これで、定義は完了です。実際に正しく動くかを確認してみます。 適当にUserScopeに従うUserManagerを定義します。

// UserManager.kt
@UserScope
class UserManager @Inject constructor() {
  var value = 100
}

これはUserScopeに従うので、MainActivity、UserScopedActivityには期待通り同一インスタンスがInjectできます。

// MainActivity.kt
class MainActivity : AppCompatActivity() {

  // ok
  @Inject lateinit var userManager: UserManager

  ...
}
// UserScopedActivity.kt
class UserScopedActivity : AppCompatActivity() {

  // ok: MainActivityと同じインスタンスが注入される
  @Inject lateinit var userManager: UserManager

  ...
}

しかし、NoUserScopedActivityにInjectしようとするとコンパイルエラーになります。

// NoUserScopedActivity.kt
class NoUserScopedActivity : AppCompatActivity() {

  //  下のコメントアウトを取るとコンパイルエラー
  //  @Inject lateinit var userManager: UserManager
  ...
}

NoUserScopedActivityをUserScopeに従う形で定義してないためです。

が達成できました。

余談

そもそも、ContributesAndroidInjector定義時に、UserScopeスコープを付与してあげればいいんじゃないかと思うかもしれません。

// MainActivityModule.kt
@Module
interface MainActivityModule {
  @UserScope
  @ContributesAndroidInjector
  fun contributeMainActivity(): MainActivity
}

// UserScopedActivityModule.kt
@Module
interface UserScopedActivityModule {
  @UserScope
  @ContributesAndroidInjector
  fun contributeUserScopedActivity(): UserScopedActivity
}

このやり方だと、今回のケースには不都合です。

MainActivity、UserScopedActivityはUserScopeに従うのでコンパイルは通ります。 しかし、MainActivity、UserScopedActivityで同一インスタンスを使うことは出来ません。

何故かと言うと、ContributesAndroidInjectorはSubcomponentを作るシンタックスシュガーのようなものですが、 MainActivityModuleUserScopedActivityModuleはそれぞれ独立したSubcomponentを作るので、独立したComponent間で同一インスタンスを使うことが出来ないためです。

今回のように、UserSubcomponentを定義して、そのComponentをベースに所属させる必要があります。

まとめ

Written by
あんどろいどでぃべろっぱぁー🍎