- @Singleton
- @Component(
- modules = [
- AppModule::class,
- ]
- )
- interface AppComponent {
- fun inject(app: MainActivity)
- }
- @Module
- class AppModule {
- @Singleton
- @Provides
- fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {
- return NeedResetRepository(myApi)
- }
- @Singleton
- @Provides
- fun provideMyApi(): MyApi {
- return MyApi()
- }
- }
- class NeedResetRepository(
- private val myApi: MyApi
- ) {
- // キャッシュなどのデータを持っている
- }
- class MyApi {
- // 状態を持たない
- }
ログアウト時に NeedResetRepository で保持しているキャッシュを削除したいので、MyApplication で保持している AppComponent のインスタンスを null にしてから MainActivity を作り直しています(reset() メソッドのところ)。
(Hilt がない時代の標準的なやり方だと AppComponent のインスタンスは lateinit var にして onCreate() で代入し、作り直すことがないようにすることが多いのですが、それだと Hilt への移行で困ることがないので、今回は困るパターンということでこのような構成例になっています)
- class MyApplication : Application() {
- private var appComponent: AppComponent? = null
- fun getAppComponent(): AppComponent {
- return appComponent ?: DaggerAppComponent.builder()
- .build()
- .also {
- appComponent = it
- }
- }
- fun reset() {
- appComponent = null
- TaskStackBuilder.create(this)
- .addNextIntent(
- Intent(this, MainActivity::class.java)
- )
- .startActivities()
- }
- }
AppComponent と NeedResetRepository には @Singleton がついているので、AppComponent が作り直されるまで NeedResetRepository のインスタンスは変わりません。そのため例えば画面回転時には NeedResetRepository のインスタンスは変わりません。
- class MainActivity : ComponentActivity() {
- @Inject
- lateinit var needResetRepository: NeedResetRepository
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- val appComponent = (application as MyApplication).getAppComponent()
- appComponent.inject(this)
- enableEdgeToEdge()
- setContent {
- MaterialTheme {
- Scaffold { innerPadding ->
- Column(
- verticalArrangement = Arrangement.spacedBy(8.dp),
- modifier = Modifier
- .padding(innerPadding)
- .padding(16.dp),
- ) {
- Text(
- text = "$needResetRepository",
- )
- Button(
- onClick = {
- (application as MyApplication).reset()
- }
- ) {
- Text("reset")
- }
- }
- }
- }
- }
- }
- }
ステップ1 : Hilt ライブラリの設定
libs.versions.toml
- [libraries]
- ...
- hilt = { module = "com.google.dagger:hilt-android", version.ref = "dagger" }
- hilt-compiler = { module = "com.google.dagger:hilt-android-compiler", version.ref = "dagger" }
- [plugins]
- ...
- hilt = { id = "com.google.dagger.hilt.android", version.ref = "dagger" }
- plugins {
- ...
- alias(libs.plugins.hilt) apply false
- }
- plugins {
- ...
- + alias(libs.plugins.hilt)
- }
- ...
- dependencies {
- - implementation(libs.dagger)
- - ksp(libs.dagger.compiler)
- + implementation(libs.hilt)
- + ksp(libs.hilt.compiler)
- ...
- }
hilt の plugin を build.gradle に設定し、dependencies の依存ライブラリを dagger のものから hilt のものに置き換えてビルドすると、次のようなエラーがでます。
[ksp] /.../AppComponent.kt:21: [Hilt] com.example.sample.AppModule is missing an @InstallIn annotation. If this was intentional, see https://dagger.dev/hilt/flags#disable-install-in-check for how to disable this check.
@Module アノテーションがついている AppModule に @InstallIn がついていないと怒られていますが、AppModule は AppComponent 用なので @DisableInstallInCheck をつけてエラーがでないようにします。
- @DisableInstallInCheck
- @Module
- class AppModule {
- ...
- }
- @HiltAndroidApp
- class MyApplication : Application() {
- ...
- }
ステップ2 : SubComponent 化
AppModule を分割して、リセットする必要のない MyApi は @InstallIn(SingletonComponent::class) をつけた Module で管理するようにしたいのですが、
- @InstallIn(SingletonComponent::class)
- @Module
- object AppModule2 {
- @Singleton
- @Provides
- fun provideMyApi(): MyApi {
- return MyApi()
- }
- }
[ksp] /.../AppComponent.kt:17: [Dagger/MissingBinding] com.example.sample.MyApi cannot be provided without an @Inject constructor or an @Provides-annotated method.
このようなエラーが出てしまいます。
そこで、Module を分割する前に、既存の AppComponent を Hilt の SubComponent に変更します。
1. AppComponent のアノテーションを @Subcomponent に変更し、@Subcomponent.Builder をつけた Builder を用意します
- -@Singleton
- -@Component(
- - modules = [
- - AppModule::class,
- - ]
- -)
- +@Subcomponent
- interface AppComponent {
- + @Subcomponent.Builder
- + interface Builder {
- + fun build(): AppComponent
- + }
- +
- fun inject(app: MainActivity)
- }
2. 任意のタイミングで AppComponent を作り直せるように、AppComponent の親 Component を用意します。ログアウト時にリセットすることを想定して、ここでは AuthComponent という名前にしています。
AuthComponent では SingletonComponent に InstallIn されている型も見えるようにしたいので、parent に SingletonComponent を指定します。
AuthComponent に対応する Scope も用意します。ここでは AuthScope という名前にしています。AuthComponent に @AuthScope をつけます。
- @Scope
- @Retention(AnnotationRetention.RUNTIME)
- annotation class AuthScope
- @AuthScope
- @DefineComponent(parent = SingletonComponent::class)
- interface AuthComponent {
- @DefineComponent.Builder
- interface Builder {
- fun build(): AuthComponent
- }
- }
- @InstallIn(AuthComponent::class)
- @Module(
- subcomponents = [
- AppComponent::class,
- ]
- )
- interface AuthModule
3. AppModule の @DisableInstallInCheck を @InstallIn(AuthComponent::class) に変更し、@Singleton を @AuthScope に変更します。
- -@DisableInstallInCheck
- +@InstallIn(AuthComponent::class)
- @Module
- object AppModule {
- - @Singleton
- + @AuthScope
- @Provides
- fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {
- return NeedResetRepository(myApi)
- }
- - @Singleton
- + @AuthScope
- @Provides
- fun provideMyApi(): MyApi {
- return MyApi()
- }
- }
4. 任意のタイミングで AppComponent を作り直すために、AppComponent の親である AuthComponent を管理する GeneratedComponentManager を用意します。
- @Singleton
- class AuthComponentRegistry @Inject constructor(
- private val authComponentBuilder: AuthComponent.Builder,
- ) : GeneratedComponentManager<AuthComponent> {
- private var authComponent: AuthComponent
- init {
- authComponent = authComponentBuilder.build()
- }
- fun reset() {
- authComponent = authComponentBuilder.build()
- }
- override fun generatedComponent(): AuthComponent {
- return authComponent
- }
- fun getAppComponent(): AppComponent {
- return EntryPoints.get(
- this,
- AuthComponentEntryPoint::class.java
- )
- .appComponentBuilder()
- .build()
- }
- @EntryPoint
- @InstallIn(AuthComponent::class)
- interface AuthComponentEntryPoint {
- fun appComponentBuilder(): AppComponent.Builder
- }
- }
AppComponent.Builder は AuthModule で @InstallIn(AuthComponent::class) されているので、同じように @InstallIn(AuthComponent::class) をつけた EntryPoint を用意することで取得できます。
この EntryPoint のインスタンスは、EntryPoints.get() に AuthComponentRegistry インスタンスを渡すことで取得できます。
5. MyApplication で保持している appComponent を削除して、getAppComponent() では AuthComponentRegistry から取得したインスタンスを返すようにします。reset() メソッドでは AuthComponentRegistry の reset() メソッドを呼ぶように変更します。
- @HiltAndroidApp
- class MyApplication : Application() {
- - private var appComponent: AppComponent? = null
- + @Inject
- + lateinit var authComponentRegistry: AuthComponentRegistry
- fun getAppComponent(): AppComponent {
- - return appComponent ?: DaggerAppComponent.builder()
- - .build()
- - .also {
- - appComponent = it
- - }
- + return authComponentRegistry.getAppComponent()
- }
- fun reset() {
- - appComponent = null
- + authComponentRegistry.reset()
これで AppComponent が Hilt で管理されるようになりました。
reset ボタンが押されるまでは NeedResetRepository のインスタンスが保持され、reset ボタンを押すと NeedResetRepository のインスタンスが新しくなるという以前の挙動を保っています。
ステップ3 : Module 分割
ここまでくれば MyApi を別 Module に分割できます。
- @InstallIn(AuthComponent::class)
- @Module
- object AppModule {
- @AuthScope
- @Provides
- fun provideNeedResetRepository(myApi: MyApi): NeedResetRepository {
- return NeedResetRepository(myApi)
- }
- +}
- - @AuthScope
- +@InstallIn(SingletonComponent::class)
- +@Module
- +object AppModule2 {
- +
- + @Singleton
- @Provides
- fun provideMyApi(): MyApi {
- return MyApi()
- }
- }
ステップ4 : @HiltViewModel
この段階で、AuthComponent に依存しないクラスだけを引数にとる ViewModel なら @HiltViewModel を使えるようになります。
- @HiltViewModel
- class SomeViewModel @Inject constructor(
- prival val myApi: MyApi
- ) : ViewModel()
- @AndroidEntryPoint
- class SomeActivity : ComponentActivity() {
- private val viewModel by viewModels<SomeViewModel>()
- }
AuthComponent に依存するクラスを引数にとるときは AssistedInject を利用します。
https://dagger.dev/hilt/view-model#assisted-injection
- @HiltViewModel(assistedFactory = SomeViewModel.Factory::class)
- class SomeViewModel @AssistedInject constructor(
- @Assisted private val needResetRepository: NeedResetRepository
- ) : ViewModel() {
- @AssistedFactory
- interface Factory {
- fun create(
- needResetRepository: NeedResetRepository
- ): SomeViewModel
- }
- }
- @AndroidEntryPoint
- class SomeActivity : ComponentActivity() {
- private val viewModel by viewModels<SomeViewModel>(
- extrasProducer = {
- defaultViewModelCreationExtras.withCreationCallback<SomeViewModel.Factory> { factory ->
- factory.create(
- authComponentEntryPoint().needResetRepository()
- )
- }
- }
- )
- }
- @EntryPoint
- @InstallIn(AuthComponent::class)
- interface AuthComponentEntryPoint {
- fun needResetRepository(): NeedResetRepository
- }
- @InstallIn(SingletonComponent::class)
- @EntryPoint
- interface SingletonComponentEntryPoint {
- fun authComponentRegistry(): AuthComponentRegistry
- }
- fun Context.authComponentEntryPoint(): AuthComponentEntryPoint {
- val authComponentRegistry = EntryPointAccessors
- .fromApplication<SingletonComponentEntryPoint>(this)
- .authComponentRegistry()
- return EntryPoints.get(authComponentRegistry, AuthComponentEntryPoint::class.java)
- }
ステップ4 : AppComponent の inject() メソッド廃止
上で用意した便利メソッドを使って MainActivity の needResetRepository にインスタンスをセットするように変えます。
MyApi は @InstallIn(SingletonComponent::class) なので MainActivity に @AndroidEntryPoint をつければ inject されます。
- +@AndroidEntryPoint
- class MainActivity : ComponentActivity() {
- - @Inject
- - lateinit var needResetRepository: NeedResetRepository
- + private lateinit var needResetRepository: NeedResetRepository
- @Inject
- lateinit var myApi: MyApi
- override fun onCreate(savedInstanceState: Bundle?) {
- super.onCreate(savedInstanceState)
- - val appComponent = (application as MyApplication).getAppComponent()
- - appComponent.inject(this)
- + needResetRepository = authComponentEntryPoint().needResetRepository()
- enableEdgeToEdge()
- interface AppComponent {
- interface Builder {
- fun build(): AppComponent
- }
- - fun inject(app: MainActivity)
- }
ステップ5 : AppComponent の廃止
AppComponent に定義されているメソッドがなくなったら AppComponent 自体を廃止します。
- -@Subcomponent
- -interface AppComponent {
- -
- - @Subcomponent.Builder
- - interface Builder {
- - fun build(): AppComponent
- - }
- -}
- -@InstallIn(AuthComponent::class)
- -@Module(
- - subcomponents = [
- - AppComponent::class,
- - ]
- -)
- -interface AuthModule
- @Singleton
- class AuthComponentRegistry @Inject constructor(
- private val authComponentBuilder: AuthComponent.Builder,
- ) : GeneratedComponentManager<AuthComponent> {
- ...
- override fun generatedComponent(): AuthComponent {
- return authComponent
- }
- -
- - fun getAppComponent(): AppComponent {
- - return EntryPoints.get(
- - this,
- - AuthComponentEntryPoint::class.java
- - )
- - .appComponentBuilder()
- - .build()
- - }
- -
- - @EntryPoint
- - @InstallIn(AuthComponent::class)
- - interface AuthComponentEntryPoint {
- - fun appComponentBuilder(): AppComponent.Builder
- - }
- }
- @HiltAndroidApp
- class MyApplication : Application() {
- @Inject
- lateinit var authComponentRegistry: AuthComponentRegistry
- - fun getAppComponent(): AppComponent {
- - return authComponentRegistry.getAppComponent()
- - }
- -
- fun reset() {
これで Hilt への移行完了です!