Search code examples
androiddaggerdagger-hilt

Dagger Hilt provide alternative Modules for different flavors/build types


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 NetworkModules 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?


Solution

  • The key thing with Hilt is that by default the modules in your source code = the modules installed in your app.

    Option 1: Separate Code paths

    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 */ }
    }
    

    Option 2: Override using @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 { ... }
    }
    

    Option 3: Multibinding @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