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?
A Hilt module is a class that is annotated with @Module . Like a Dagger module, it informs Hilt how to provide instances of certain types. Unlike Dagger modules, you must annotate Hilt modules with @InstallIn to tell Hilt which Android class each module will be used or installed in.
Dagger and Hilt code can coexist in the same codebase. However, in most cases, it's best to use Hilt to manage all your use of Dagger on Android.
Hilt provides a standard way to incorporate Dagger dependency injection into an Android application. The goals of Hilt are: To simplify Dagger-related infrastructure for Android apps. To create a standard set of components and scopes to ease setup, readability/understanding, and code sharing between apps.
Dependency injection (DI) is a technique widely used in programming and well suited to Android development, where dependencies are provided to a class instead of creating them itself. By following DI principles, you lay the groundwork for good app architecture, greater code reusability, and ease of testing.
Declare an @EntryPoint interface in the app module (or in any other module that can be processed by Hilt) with the dependencies that the feature module needs. Create a Dagger component that depends on the @EntryPoint interface. Use Dagger as usual in the feature module.
A Hilt module is a class that is annotated with @Module. Like a Dagger module, it informs Hilt how to provide instances of certain types. Unlike Dagger modules, you must annotate Hilt modules with @InstallIn to tell Hilt which Android class each module will be used or installed in. Note: Hilt modules are different from Gradle modules.
Now, with Dagger-Hilt releasing as a part of Jetpack libraries, it is now the recommended way by Google to use it. According to Dagger-Hilt, it helps us: To make the dagger code easy and simple for developers.
This requires marking your Dagger modules with Hilt annotations to tell Hilt which component they should go into. Getting objects in your Android framework classes is done by using another Hilt annotation which will generate the Dagger injection code into a base class that you will extend.
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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With