Dagger Hiltへのマイグレーションのコードラボは数ヶ月前にやっていて忘れてきたので、どういう内容だったのか後から振り替えれるようにグラフをおいておきます。
ちょっと先にDagger Hiltのチュートリアル的なのは他の記事などでご確認ください。
またDagger SPIを使ったいい感じのGradle Pluginも見つけたのでそれで見ていきます。
https://github.com/arunkumar9t2/scabbard
今Daggerをグラフ化して見るのはDaggerの公式SPI(Service provider interface)を使っているこれが良さそうかも👀 https://t.co/IzVaZJofuo
— takahirom (@new_runnable) September 25, 2020
マイグレーション前
Daggerでは、DaggerのComponent、DIコンテナでオブジェクトをインスタンス化するためのロジック(コンストラクションロジック)とインスタンスが保持されています。
AppComponentにはアプリケーション全体で保持するコンストラクションロジックとインスタンスを保持しており、RegistrationComponentには登録のときに使うコンストラクションロジックとインスタンスが入っています。
コンポーネントの木
今回はSubComponentという仕組みが使われており、コンポーネント同士が依存することができ木構造になっており、RegistrationComponentではAppComponentの内容が使えるようになっています。
コードでのSubComponentの作り方(詳細は説明しません)
@Singleton
@Component(modules = [StorageModule::class, AppSubcomponents::class])
interface AppComponent {
...
// サブコンポーネントのFactoryを取得できるようにする
fun registrationComponent(): RegistrationComponent.Factory
fun loginComponent(): LoginComponent.Factory
...
}
@Module(
subcomponents = [ // サブコンポーネントを指定する
RegistrationComponent::class,
LoginComponent::class,
UserComponent::class
]
)
class AppSubcomponents
@ActivityScope
@Subcomponent
interface RegistrationComponent {
@Subcomponent.Factory
interface Factory {
fun create(): RegistrationComponent
}
...
}
そしてRegistrationComponentとLoginComponentでは @ActivityScope
と書いており、これを使うことでこのComponentのDIコンテナを指定してインスタンスを配布したり、同じインスタンスを共有したりできるようにしているようです。 (このサンプルでは、ViewModelがこの@ActivityScope
になっている)
@ActivityScope
@Subcomponent
interface LoginComponent {
...
}
@ActivityScope
class RegistrationViewModel @Inject constructor(val userManager: UserManager) {
中間までマイグレーション
Dagger HiltではDaggerのアプリのコンポーネントの構造を標準化するので、このようにActivityComponent(内部的にはActivityRetainedC)などのComponentが追加されています。
中間までのマイグレーションなので、UserComponentが残っています。
Dagger Hiltのコード生成によって、ActivityComponent(内部的にはActivityRetainedC)など勝手にSubCompopnentが作られます。
しかし、UserComponentは自分で作ったComopnentなので、生成させられません。
どのようにしたかというと、InstallIn
でApplicationComponentにModuleを追加し、そこでsubcomponentsを指定しています。
@InstallIn(ApplicationComponent::class)
@Module(
subcomponents = [
UserComponent::class
]
)
class AppSubcomponents
そしてUserComponent.FactoryをUserManagerにInjectさせることで、UserComponentを作成し利用できます。
@Singleton
class UserManager @Inject constructor(
private val storage: Storage,
private val userComponentFactory: UserComponent.Factory
) {
ただ、UserComponentはDagger Hiltの外のComponentなので、このUserComponentの管理は自分で行う必要があります。
@Singleton
class UserManager @Inject constructor(
private val storage: Storage,
private val userComponentFactory: UserComponent.Factory
) {
var userComponent: UserComponent? = null
private set
...
fun registerUser(username: String, password: String) {
...
userJustLoggedIn()
}
fun loginUser(username: String, password: String): Boolean {
...
userJustLoggedIn()
return true
}
fun logout() {
// When the user logs out, we remove the instance of UserComponent from memory
userComponent = null
}
private fun userJustLoggedIn() {
userComponent = userComponentFactory.create()
}
}
ここでUserComponentによるInjectを使う上で一つ問題があります。
MainActivityでUserComponentによるInjectが使うためには、UserManagerのインスタンスが必要です。UserComponentのDIコンテナでInjectしたいのでMainActivityには@AndroidEntryPoint
をつけてHiltのInjectを使うことができません。
こういうときにDagger HiltのDIコンテナが持つインスタンスを受け取る方法があります。 EntryPointAccessor
を使うというもので、これによってUserManagerを取得して、UserManagerのもつuserComponentでinjectが可能になります。
class MainActivity : AppCompatActivity() {
@InstallIn(ApplicationComponent::class)
@EntryPoint
interface UserManagerEntryPoint {
fun userManager(): UserManager
}
...
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Grabs instance of UserManager from the application graph
val entryPoint = EntryPointAccessors.fromApplication(applicationContext, UserManagerEntryPoint::class.java)
val userManager = entryPoint.userManager()
...
userManager.userComponent!!.inject(this)
...
}
マイグレーション後
完全にUserComponentが削除されており、HiltのComponentが完全に使われています。
ちなみに@Singleton
なUserRepositoryを使って、その中でログイン中かなどを管理することで、解決しているようです。