@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 {
// 状態を持たない
}
この構成では MyApplication で Dagger の Component である AppComponent のインスタンスを保持しています。
ログアウト時に 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()
}
}
以下のコードで MainActivity の reset ボタンを押すと、AppComponent のインスタンスが新しくなるので AppComponent および NeedResetRepository のインスタンスが新しくなるのがわかります。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")
}
}
}
}
}
}
}
さて、このような構成に対し、新しく作成するクラスや ViewModel では Hilt を使いたいとします。
ステップ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" }
build.gradle
plugins {
...
alias(libs.plugins.hilt) apply false
}
app/build.gradle
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 {
...
}
最後に MyApplication に @HiltAndroidApp アノテーションをつけます。
@HiltAndroidApp
class MyApplication : Application() {
...
}
ここまでで、ビルドして以前と同じ動作になっているのが確認できます(MainActivity にはまだ @AndroidEntryPoint はつけません!)。
ステップ2 : SubComponent 化
AppModule を分割して、リセットする必要のない MyApi は @InstallIn(SingletonComponent::class) をつけた Module で管理するようにしたいのですが、
@InstallIn(SingletonComponent::class)
@Module
object AppModule2 {
@Singleton
@Provides
fun provideMyApi(): MyApi {
return MyApi()
}
}
こうすると、NeedResetRepository を @Provides しているところで 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
}
}
AppComponent を AuthComponent に紐づけるための Module を用意します。@InstallIn(AuthComponent::class) をつけ、@Module の subcomponents で AppComponent を指定します。
@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
}
}
reset() メソッドを用意して、そこで authComponent のインスタンスを作り直すことによって、任意のタイミングで AppComponent を作り直せるようにしています。
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()
}
}
AppModule2 には @InstallIn(SingletonComponent::class) をつけ、provideMyApi() の scope を @AuthScope から @Singleton に変更します。
reset ボタンが押されても 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()
)
}
}
)
}
Context から AuthComponent に依存するクラスのインスタンスを取れるように便利メソッドを用意しておきます。
@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)
}
このように AppComponent の inject() メソッドを段階的に廃止していきます。
ステップ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 への移行完了です!
0 件のコメント:
コメントを投稿