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
,这样如果没有任何内容绑定到映射中,事情就会在编译时而不是运行时失败
我尝试将应用程序迁移到 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
,这样如果没有任何内容绑定到映射中,事情就会在编译时而不是运行时失败