Dagger-AndroidでUserScopeのようなカスタムのScopeを使い、特定のActivity間のみで同一インスタンスを使う方法
Created at Tue, Jun 26, 2018Daggerを使い、複数のインスタンス間で同一のインスタンスを使う時は、スコープを使うことで実現できます。
Androidでは、すべてのActivityで共通のインスタンスを使うには Singleton
スコープとAppComponentを組み合わせて使う方法がよく知られています。
しかし、__特定__のActivity間でのみ共通のインスタンスを使いたい場合にはこの方法は使えません。Singletonだと__すべて__のActivity間で共通のインスタンスが使えてしまいます。
この記事では、Dagger-Androidを使ったサンプルコードをベースに、「特定のActivity間のみで同一インスタンスを使う方法」を説明します。 また、基本的なDaggerの使い方は知っている前提で説明していきます。
サンプルコードはこちらになります。 コードを見ると理解がより深まると思うので、ぜひご覧になってください😊
では説明していきます。今回のサンプルコードの目指すところは
- UserScopeを定義し、MainActivity、UserScopedActivityで同一の
UserManager
インスタンスを使用する
とします。
まず最初に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
を定義することです。
こうすることで、AppComponent
にUserSubcomponent
を結び付けることが出来ます。
これで基本的な部分の定義は完了しました。
次に、各ActivityをComponentに結びつけていきます。 サンプルではMainActivity、UserScopedActivityとNoUserScopedActivityの3つのActivityを定義しており、 それぞれのActivityは以下のように振る舞わせたいとします。
- MainActivity、UserScopedActivityはUserScopeに従い、インスタンスを共通で使いたい
- NoUserScopedActivityはUserScopeに従わない、コンパイルエラーにしたい
MainActivity、UserScopedActivityをUserSubcomponentに従わせる
MainActivity、UserScopedActivityをUserSubcomponent
に定義することで、MainActivity、UserScopedActivityをUserScope
に従わせることが出来ます。
なぜなら、UserSubcomponent
はUserScope
に紐付いているためです。
// 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
を使っていたらコンパイルエラーにすることが出来ます。
AppComponent
はUserScope
に紐付いていないためです。
// 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
に従う形で定義してないためです。
- MainActivity、UserScopedActivityはUserScopeに従い、インスタンスを共通で使いたい
- 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を作るシンタックスシュガーのようなものですが、
MainActivityModule
とUserScopedActivityModule
はそれぞれ独立したSubcomponentを作るので、独立したComponent間で同一インスタンスを使うことが出来ないためです。
今回のように、UserSubcomponent
を定義して、そのComponentをベースに所属させる必要があります。
まとめ
- 特定のActivityのみで共通のインスタンスを使いたいときは、結構めんどう
- 冗長な気がするので、もっといい方法があったら教えてください😋
ContributesAndroidInjector
がどういう動作をするのかを知っておくと、いざというときに便利- サンプルコードはこちら