Dagger Hilt 为不同的 flavors/build 类型提供替代模块

Dagger Hilt provide alternative Modules for different flavors/build types

我尝试将应用程序迁移到 Dagger Hilt。在我的旧设置中,我将模块切换为调试版本中的调试版本或不同的产品风格。例如:

@Module
open class NetworkModule {

    @Provides
    @Singleton
    open fun provideHttpClient(): OkHttpClient {
        ...
    }
}

class DebugNetworkModule : NetworkModule() {

    override fun provideHttpClient(): OkHttpClient {
        ...
    }
}

然后我在调试版本中交换了正确的模块:

val appComponent = DaggerAppComponent.builder().networkModule(DebugNetworkModule())

由于 Hilt 负责管理 ApplicationComponent,我看不出更换模块的可能性。

然而,当我查看生成的源代码时(对我来说:DaggerApp_HiltComponents_ApplicationC),我发现 Hilt 确实为不同的模块(在 ApplicationContextModule 旁边未使用)生成了一个构建器.

知道这不是最佳做法。为每个构建 type/product 风格提供不同的 NetworkModule 会更干净。但这会导致大量重复代码。

在测试中,我可以卸载模块并安装测试模块。但这在生产代码中似乎是不可能的。

还有其他方法可以实现我的目标吗?

Hilt 的关键在于,默认情况下源代码中的模块 = 应用中安装的模块。

选项 1:单独的代码路径

理想情况下,您应该为不同的构建提供替代模块,并通过 sourceSets

区分使用哪些模块

在发布源集中:

@InstallIn(ApplicationComponent::class) 
@Module
object ReleaseModule {
  @Provides
  fun provideHttpClient(): OkHttpClient { /* Provide some OkHttpClient */ }
}

在调试源集中:

@InstallIn(ApplicationComponent::class) 
@Module
object DebugModule {
  @Provides
  fun provideHttpClient(): OkHttpClient { /* Provide a different OkHttpClient */ }
}

选项 2:使用 @BindsOptionalOf

覆盖

如果选项 1 不可行,因为您想覆盖源代码中仍然存在的模块,您可以使用 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
}

然后仅在调试配置的文件中:

@InstallIn(ApplicationComponent::class) 
@Module
object DebugHttpClientModule {
  @Provides 
  @DebugHttpClient
  fun provideHttpClient(): OkHttpClient { ... }
}

选项 3:多重绑定 @IntoMap

如果您需要比实现 + test/debug 覆盖更多的粒度,您可以使用多重绑定和映射,使用键作为选择实现的优先级。

@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" }
  }
}

主要应用模块:

@InstallIn(ApplicationComponent::class) 
@Module
object MainModule {
  @Provides 
  @IntoMap 
  @IntKey(0)
  fun provideDefaultHttpClient(): OkHttpClient {
    ...
  }
}

调试覆盖:

@InstallIn(ApplicationComponent::class) 
@Module
object DebugModule {
  @Provides 
  @IntoMap 
  @IntKey(1)
  fun provideDebugHttpClient(): OkHttpClient {
    ...
  }
}

如果您使用选项 3,我会生成提供的类型 nullable/optional,或者避免使用 @Multibinds,这样如果没有任何内容绑定到映射中,事情就会在编译时而不是运行时失败