Dagger / Hilt
Hilt - слой внедрения зависимостей для Android, построенный поверх Dagger. Он сохраняет генерацию кода и проверку графа на этапе компиляции, но предоставляет стандартную иерархию компонентов, связанную с жизненным циклом Android.
Hilt и Dagger
Что такое Hilt?
Hilt стандартизирует подключение графа Dagger к Android-приложению:
@HiltAndroidAppсоздаёт контейнер уровня приложения.@AndroidEntryPointвключает внедрение зависимостей в создаваемые системой классы Android, например activity, fragment, service и receiver.@HiltViewModelинтегрируетViewModelсViewModelProvider.@Injectотмечает конструкторы или поля для внедрения зависимостей.
В типичном Android-приложении больше не нужно вручную описывать компонент приложения, Android-подкомпоненты и их фабрики. При этом базовые понятия Dagger остаются важными: bindings, modules, qualifiers, scopes и ошибки графа зависимостей.
Hilt и Dagger
Dagger - универсальный DI-фреймворк, работающий на этапе компиляции. Он генерирует код создания объектов и проверяет, что для каждой запрошенной зависимости существует ровно один подходящий binding, а в графе нет циклов.
Hilt - специализированная Android-интеграция поверх Dagger. Он предоставляет готовые компоненты и связывает их с владельцами жизненного цикла Android. Для современного Android-приложения Hilt обычно является практичным выбором по умолчанию. Чистый Dagger остаётся полезен для legacy-графов, не-Android кода и архитектур, которым нужен нестандартный контроль над компонентами.
Bindings
@Inject
Для собственных классов предпочтительно внедрение через конструктор: зависимости видны явно, а объект легко создать в тесте.
Если для каждого параметра конструктора есть binding, отдельный provider не требуется. В конструктор стоит передавать зависимости создания, а изменяющиеся данные конкретной операции - в методы, не добавляя каждое runtime-значение в DI-граф.
Внедрение через поля в основном нужно для объектов, которые создаёт Android framework. Такое поле не может быть private и недоступно до момента, когда Hilt выполнит injection в соответствующем lifecycle callback. Обычным классам приложения лучше использовать конструктор.
@Provides и @Binds
Используйте @Binds, чтобы связать injectable-реализацию с абстракцией:
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
abstract fun bindUserRepository(
implementation: DefaultUserRepository,
): UserRepository
}
Используйте @Provides, когда для создания нужен код или у класса не может быть @Inject-конструктора, например для Retrofit, OkHttp, Room, DataStore или внешнего SDK:
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApi(client: OkHttpClient): ApiService =
Retrofit.Builder()
.baseUrl("https://example.com/")
.client(client)
.build()
.create(ApiService::class.java)
}
Provider-методы должны содержать создание и связывание объектов, а не бизнес-правила. Ни @Binds, ни @Provides сами по себе не делают объект singleton: временем жизни управляет отдельная scope-аннотация.
Qualifiers
Если в графе есть несколько bindings одного типа, различайте их с помощью qualifier. Обычно собственная аннотация с предметным названием безопаснее, чем строковый @Named, в котором легко допустить опечатку.
@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class AuthenticatedClient
@Provides
@AuthenticatedClient
fun provideAuthenticatedClient(
authInterceptor: AuthInterceptor,
): OkHttpClient = OkHttpClient.Builder()
.addInterceptor(authInterceptor)
.build()
Одинаковый qualifier должен стоять и на binding, и в месте injection. Лучше последовательно отмечать им все bindings данного типа, чтобы выбор был явным.
@Module / @InstallIn
@Module объединяет bindings, которые нельзя выразить через constructor injection. @InstallIn выбирает сгенерированный компонент Hilt, которому они принадлежат, и определяет область их видимости.
Binding родительского компонента доступен дочерним компонентам, но binding дочернего компонента недоступен родителю и соседним компонентам. Устанавливайте binding в самый нижний компонент, охватывающий всех потребителей. Так screen-specific зависимость не станет доступна всему приложению, а объект со scope приложения не получит недопустимый activity context.
Для известных Hilt типов используйте готовые bindings и qualifiers, например @ApplicationContext и @ActivityContext, вместо дублирующих context providers.
Компоненты и ViewModel
Компоненты и scopes Hilt
Основные пары компонентов и scopes:
| Компонент | Типичное время жизни | Соответствующий scope |
|---|---|---|
SingletonComponent |
Граф процесса приложения | @Singleton |
ActivityRetainedComponent |
Логическая activity между изменениями конфигурации | @ActivityRetainedScoped |
ViewModelComponent |
Одна ViewModel |
@ViewModelScoped |
ActivityComponent |
Один экземпляр activity | @ActivityScoped |
FragmentComponent |
Один экземпляр fragment | @FragmentScoped |
ServiceComponent |
Один экземпляр service | @ServiceScoped |
Scope означает один экземпляр на один экземпляр компонента, а не обязательно один объект на всё приложение. Для unscoped binding новый объект может создаваться при каждом запросе. Scope нужен объектам с общей идентичностью, собственными ресурсами или дорогим созданием. Stateless use cases и mappers часто можно оставить без scope.
ActivityRetainedComponent переживает изменение конфигурации, а ActivityComponent - нет. Ни один из них не следует использовать для UI state, который должен храниться во ViewModel или saved state.
Внедрение в ViewModel
Отметьте ViewModel аннотацией @HiltViewModel, а зависимости передайте через конструктор:
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val repository: UserRepository,
savedStateHandle: SavedStateHandle,
) : ViewModel()
Activity или fragment, которым принадлежит ViewModel, должны быть отмечены @AndroidEntryPoint. В Compose аннотация на composable не ставится: ViewModel получают через Hilt-aware API с подходящим владельцем из activity или navigation.
Используйте SavedStateHandle для navigation arguments и восстанавливаемого состояния экрана. Если обязательный runtime-аргумент не относится к saved state, применяйте assisted injection Hilt. ViewModel не должна получать или хранить Activity, Fragment, View либо UI context. Если доступ уровня приложения действительно необходим, используйте абстракцию или @ApplicationContext, оставляя UI-работу за пределами ViewModel.
Диагностика ошибок графа
Большинство ошибок Hilt обнаруживается на этапе компиляции. Начните с первого сообщения об отсутствующем или дублирующемся binding, затем проследите показанную Dagger цепочку зависимостей.
Частые причины:
- отсутствует constructor или module binding;
- для одного типа есть два bindings без qualifier;
- qualifier у provider и consumer не совпадает;
- binding установлен в компонент, который не является предком consumer;
- scoped binding зависит от объекта из более короткоживущего компонента;
- Gradle-модуль с bindings не входит в транзитивные зависимости приложения.