I try to migrate an App to Dagger Hilt. In my old setup I switched a Module for a Debug Version in Debug builds or for different product flavors. E.g.:
@Module
open class NetworkModule {
@Provides
@Singleton
open fun provideHttpClient(): OkHttpClient {
...
}
}
class DebugNetworkModule : NetworkModule() {
override fun provideHttpClient(): OkHttpClient {
...
}
}
Then I swapped in the correct Module in Debug builds:
val appComponent = DaggerAppComponent.builder().networkModule(DebugNetworkModule())
Since Hilt manages the ApplicationComponent
I see no possibility to swap in Modules.
However when I have a look into the generated source code (for me: DaggerApp_HiltComponents_ApplicationC
) I see that Hilt does generate a Builder for the different Modules (which are unused beside the ApplicationContextModule
).
I know this is not the best practice. It would be cleaner to just provide different NetworkModule
s for each build type/product flavor. But that would result in lots of duplicated code.
In Tests I can uninstall Modules and install Test Modules. But that seem to be impossible in production code.
Is there any other way to achieve my goal?
The key thing with Hilt is that by default the modules in your source code = the modules installed in your app.
Ideally, you would have alternative modules for the different builds, and separate which ones are used via sourceSets
In release source set:
@InstallIn(ApplicationComponent::class)
@Module
object ReleaseModule {
@Provides
fun provideHttpClient(): OkHttpClient { /* Provide some OkHttpClient */ }
}
In debug source set:
@InstallIn(ApplicationComponent::class)
@Module
object DebugModule {
@Provides
fun provideHttpClient(): OkHttpClient { /* Provide a different OkHttpClient */ }
}
@BindsOptionalOf
If option 1 isn't feasible because you want to override a module that's still present in the source, you could use dagger optional binding
@InstallIn(ApplicationComponent::class)
@Module
object Module {
@Provides
fun provideHttpClient(
@DebugHttpClient debugOverride: Optional<OkHttpClient>
): OkHttpClient {
return if (debugOverride.isPresent()) {
debugOverride.get()
} else {
...
}
}
}
@Qualifier annotation class DebugHttpClient
@InstallIn(ApplicationComponent::class)
@Module
abstract class DebugHttpClientModule {
@BindsOptionalOf
@DebugHttpClient
abstract fun bindOptionalDebugClient(): OkHttpClient
}
and then in a file only in the debug configuration:
@InstallIn(ApplicationComponent::class)
@Module
object DebugHttpClientModule {
@Provides
@DebugHttpClient
fun provideHttpClient(): OkHttpClient { ... }
}
@IntoMap
If you need more granularity that just implmenentation + test/debug override, you could use multibinding and maps, using the key as the priority for which implementation to choose.
@InstallIn(ApplicationComponent::class)
@Module
object Module {
@Provides
fun provideHttpClient(
availableClients: Map<Int, @JvmSuppressWildcards OkHttpClient>
): OkHttpClient {
// Choose the available client from the options provided.
val bestEntry = availableClients.maxBy { it.key }
return checkNotNull(bestEntry?.value) { "No OkHttpClients were provided" }
}
}
Main application module:
@InstallIn(ApplicationComponent::class)
@Module
object MainModule {
@Provides
@IntoMap
@IntKey(0)
fun provideDefaultHttpClient(): OkHttpClient {
...
}
}
Debug override:
@InstallIn(ApplicationComponent::class)
@Module
object DebugModule {
@Provides
@IntoMap
@IntKey(1)
fun provideDebugHttpClient(): OkHttpClient {
...
}
}
If you use option 3, I would either make the provided type nullable/optional, or refrain from using @Multibinds
so that things fail at compile time rather than runtime if nothing is bound into the map