Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 798
dipdipdip Avatar asked Jun 15 '20 18:06

dipdipdip


People also ask

What is module in Dagger Hilt?

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.

Which is better Dagger or Hilt?

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.

What is the use of Dagger Hilt?

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.

What is Hilt dependency injection?

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.

How do I integrate hilt with dagger?

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.

What is a hilt 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.

What is dagger-hilt and why do we need it?

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.

How do I get objects from a dagger module to another?

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.


1 Answers

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

like image 110
Stevie Kideckel Avatar answered Nov 06 '22 10:11

Stevie Kideckel