Dagger Hilt предоставляет альтернативные модули для разных вкусов / типов сборки.

Я пытаюсь перенести приложение на 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())

Поскольку Хилт управляет ApplicationComponent, я не вижу возможности поменять местами модули.

Однако, когда я смотрю сгенерированный исходный код (для меня: DaggerApp_HiltComponents_ApplicationC), я вижу, что Hilt действительно генерирует Builder для различных модулей (которые не используются рядом с ApplicationContextModule).

Я знаю, что это не лучшая практика. Было бы проще просто предоставить разные NetworkModule для каждого типа сборки / вкуса продукта. Но это приведет к дублированию кода.

В тестах я могу удалить модули и установить тестовые модули. Но в производственном коде это кажется невозможным.

Есть ли другой способ достичь моей цели?


person dipdipdip    schedule 15.06.2020    source источник


Ответы (1)


Ключевым моментом в 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 необязательная привязка

@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

Если вам нужна большая степень детализации, а не просто переопределение внедрения + тестирования / отладки, вы можете использовать множественные привязки и карты, используя ключ в качестве приоритета для выбора реализации.

@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, я бы либо сделал предоставленный тип обнуляемым / необязательным, либо воздержался от использования @Multibinds, чтобы что-то не удалось во время компиляции, а не во время выполнения, если ничего не привязано к карте

person Stevie Kideckel    schedule 20.06.2020
comment
В реальном производственном коде есть код для вызова и получения производственного объекта от отладки и тестирования. Например, вы можете инициализировать OkHttpClient так же, как и в производственной среде, и добавить LoggingInterceptor в модуль debug Dagger. В таком случае мне интересно, как использовать рукоять кинжала. override fun provideHttpClient(): OkHttpClient { super.provideHttpClient().addNetworkInterceptor(HttpLoggingInterceptor()).build() } В настоящее время я подумываю о самостоятельной подготовке Factory и перезаписи ее исходным набором, например отладкой. - person takahirom; 15.07.2020
comment
Подойдет ли в вашем случае перевязка кинжала? dagger.dev/dev-guide/multibindings.html Вы можете связать набор NetworkInterceptors . У вас может быть поставщик @IntoSet, который присутствует только в отладочном / тестовом коде, или поставщик @ElementsIntoSet, который возвращает пустой набор в продукте и набор с элементами в зависимости от BuildConfig.DEBUG или какой-либо другой проверки времени выполнения. - person Stevie Kideckel; 16.07.2020
comment
Спасибо, что отреагировали на предложения. Этот шаблон, кажется, работает с @IntoSet и @ElementsIntoSet! Но мне нужно вызвать производственный код в нескольких других шаблонах. Например, верните объект Fake, если этот параметр включен в меню «Отладка». override fun provideApi() {if (fakeDebugEnabled) {FakeApi()} else {super.provideApi() }}. Есть и другие шаблоны, которые можно обернуть вместо возврата Fake для печати отладочной информации или изменения поведения. В таком случае это сложно. - person takahirom; 17.07.2020
comment
Если модуль будет использоваться как в режиме отладки, так и в режиме без отладки, он должен учесть возможность различных вариантов отладки тем или иным способом, либо за счет использования интерфейса с несколькими реализациями, Sets реализаций, связанных с @IntoSet/@ElementsIntoSet или Optional реализация теста с использованием @BindsOptionalOf. Вы также можете использовать квалификаторы, чтобы иметь несколько привязок для одного и того же интерфейса / класса, что может быть полезно в вашем случае. - person Stevie Kideckel; 18.07.2020