Hilt – это библиотека внедрения зависимостей для Android, которая сокращает объем шаблонного кода, необходимого для внедрения зависимостей вручную. При ручном внедрении зависимостей вам нужно вручную создавать каждый класс и его зависимости, а также использовать контейнеры для повторного использования зависимостей и управления ими.
Hilt предоставляет стандартный способ использования DI в вашем приложении, предоставляя контейнеры для каждого класса Android в вашем проекте и автоматически управляя их жизненными циклами. Hilt основан на популярной библиотеке DI Dagger, что позволяет использовать преимущества Dagger, такие как правильность во время компиляции, производительность во время выполнения, масштабируемость и поддержка Android Studio. Подробнее о Hilt и Dagger…
В этом руководстве объясняются основные понятия Hilt и сгенерированных контейнеров. Также в нем показано, как настроить существующее приложение для использования Hilt.
Добавление зависимостей
Сначала добавьте плагин hilt-android-gradle-plugin в корневой файл build.gradle проекта:
Kotlin
plugins { ... id("com.google.dagger.hilt.android") version "2.57.1" apply false }
Яркий
plugins { ... id 'com.google.dagger.hilt.android' version '2.57.1' apply false }
Затем примените плагин Gradle и добавьте эти зависимости в файл app/build.gradle:
Kotlin
plugins { id("com.google.devtools.ksp") id("com.google.dagger.hilt.android") } android { ... } dependencies { implementation("com.google.dagger:hilt-android:2.57.1") ksp("com.google.dagger:hilt-android-compiler:2.57.1") }
Яркий
... plugins { id 'com.google.devtools.ksp' id 'com.google.dagger.hilt.android' } android { ... } dependencies { implementation "com.google.dagger:hilt-android:2.57.1" ksp "com.google.dagger:hilt-compiler:2.57.1" }
Чтобы убедиться, что ваш проект настроен для Java 17, которая требуется для версий Jetpack Compose и Hilt, добавьте в файл app/build.gradle следующее:
Kotlin
android { ... compileOptions { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } }
Яркий
android { ... compileOptions { sourceCompatibility JavaVersion.VERSION_17 targetCompatibility JavaVersion.VERSION_17 } }
Класс приложения Hilt
Все приложения, использующие Hilt, должны содержать класс Application с аннотацией @HiltAndroidApp.
@HiltAndroidApp запускает генерацию кода Hilt, включая базовый класс для вашего приложения, который служит контейнером зависимостей на уровне приложения.
@HiltAndroidApp class ExampleApplication : Application() { ... }
Этот сгенерированный компонент Hilt прикреплен к жизненному циклу объекта Application и предоставляет ему зависимости. Кроме того, это родительский компонент приложения, а значит, другие компоненты могут получать доступ к предоставляемым им зависимостям.
Внедрение зависимостей в классы Android
После того как вы настроите Hilt в классе Application и станет доступен компонент уровня приложения, Hilt сможет предоставлять зависимости другим классам Android, у которых есть аннотация @AndroidEntryPoint:
@AndroidEntryPoint class ExampleActivity : ComponentActivity() { ... }
В настоящее время Hilt поддерживает следующие классы Android:
Application(с помощью@HiltAndroidApp)ViewModel(с помощью@HiltViewModel)ActivityServiceBroadcastReceiver
В Compose не нужно аннотировать отдельные composable-функции. Вместо этого добавьте к корневому элементу ComponentActivity аннотацию @AndroidEntryPoint. Это единая точка входа DI для всей иерархии пользовательского интерфейса, поэтому вы можете получать доступ к ViewModel, внедренным с помощью Hilt, непосредственно в composable-функциях.
@AndroidEntryPoint генерирует отдельный компонент Hilt для каждого класса Android в проекте. Эти компоненты могут получать зависимости от своих родительских классов, как описано в разделе Иерархия компонентов.
Чтобы получить зависимости от компонента, используйте аннотацию @Inject для внедрения через поле:
@AndroidEntryPoint class ExampleActivity : ComponentActivity() { @Inject lateinit var analytics: AnalyticsAdapter ... }
Классы, в которые Hilt внедряет зависимости, могут иметь другие базовые классы, которые также используют внедрение.
Для абстрактных классов аннотация @AndroidEntryPoint не требуется.
Подробнее о том, в какой обратный вызов жизненного цикла внедряется класс Android, рассказывается в разделе Время жизни компонента.
Определите привязки Hilt
Чтобы выполнить внедрение через поле, Hilt нужно знать, как предоставить экземпляры необходимых зависимостей из соответствующего компонента. Привязка содержит информацию, необходимую для предоставления экземпляров типа в качестве зависимости.
Один из способов предоставить Hilt информацию о привязке – внедрение через конструктор. Используйте аннотацию @Inject в конструкторе класса, чтобы указать Hilt, как предоставлять экземпляры этого класса:
class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { ... }
Параметры аннотированного конструктора класса являются зависимостями этого класса. В примере AnalyticsAdapter зависит от AnalyticsService. Поэтому Hilt также должен знать, как предоставлять экземпляры AnalyticsService.
Модули Hilt
Иногда тип нельзя внедрить через конструктор. Это может происходить по нескольким причинам. Например, вы не можете внедрить интерфейс через конструктор. Вы также не можете внедрить через конструктор тип, который вам не принадлежит, например класс из внешней библиотеки. В таких случаях вы можете предоставить Hilt информацию о привязке, используя модули Hilt.
Модуль Hilt – это класс, аннотированный с помощью @Module. Он предоставляет Hilt инструкции по созданию экземпляров типов, которые не могут быть предоставлены посредством внедрения через конструктор, например интерфейсов или сторонних классов. Вы также должны аннотировать каждый модуль с помощью @InstallIn, чтобы сообщить Hilt, в каком классе Android будет использоваться или устанавливаться каждый модуль.
Зависимости, которые вы предоставляете в модулях Hilt, доступны во всех сгенерированных компонентах, связанных с классом Android, в котором вы устанавливаете модуль Hilt.
Внедрение экземпляров интерфейса с помощью @Binds
Рассмотрим пример с AnalyticsService. Если AnalyticsService – это интерфейс,
то вы не можете внедрить его через конструктор. Вместо этого предоставьте Hilt информацию о привязке, создав абстрактную функцию с аннотацией @Binds внутри модуля Hilt.
Аннотация @Binds сообщает Hilt, какую реализацию использовать, когда нужно предоставить экземпляр интерфейса.
Аннотированная функция предоставляет Hilt следующую информацию:
- Тип возвращаемого значения функции сообщает Hilt, какой интерфейс предоставляет функция. экземпляры.
- Параметр функции сообщает Hilt, какую реализацию предоставить.
interface AnalyticsService { fun analyticsMethods() } // Constructor-injected, because Hilt needs to know how to // provide instances of AnalyticsServiceImpl, too. class AnalyticsServiceImpl @Inject constructor( ... ) : AnalyticsService { ... } @Module @InstallIn(ActivityComponent::class) abstract class AnalyticsModule { @Binds abstract fun bindAnalyticsService( analyticsServiceImpl: AnalyticsServiceImpl ): AnalyticsService }
Модуль Hilt AnalyticsModule аннотирован с помощью @InstallIn(ActivityComponent.class), потому что вы хотите, чтобы Hilt внедрил эту зависимость в ExampleActivity. Эта аннотация означает, что все зависимости в AnalyticsModule доступны во всех действиях приложения.
Внедрение экземпляров с помощью @Provides
Интерфейсы – не единственный случай, когда нельзя внедрить тип через конструктор.
Внедрение через конструктор также невозможно, если вы не являетесь владельцем класса, поскольку он получен из внешней библиотеки (например, классы Retrofit, OkHttpClient или базы данных Room), или если экземпляры должны создаваться с помощью шаблона Builder.
Рассмотрим предыдущий пример. Если вы не являетесь владельцем класса AnalyticsService, вы можете указать Hilt, как предоставлять экземпляры этого типа, создав функцию внутри модуля Hilt и аннотировав ее с помощью @Provides.
Аннотированная функция предоставляет Hilt следующую информацию:
- Тип возвращаемого значения функции сообщает Hilt, экземпляры какого типа предоставляет функция.
- Параметры функции сообщают Hilt о зависимостях соответствующего типа.
- Тело функции сообщает Hilt, как предоставить экземпляр соответствующего типа. Hilt выполняет тело функции каждый раз, когда ему нужно предоставить экземпляр этого типа.
@Module @InstallIn(ActivityComponent::class) object AnalyticsModule { @Provides fun provideAnalyticsService( // Potential dependencies of this type ): AnalyticsService { return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(AnalyticsService::class.java) } }
Предоставление нескольких привязок для одного типа
Если вам нужно, чтобы Hilt предоставлял разные реализации одного типа в качестве зависимостей, вам необходимо предоставить Hilt несколько привязок. Вы можете определить несколько привязок для одного типа с помощью квалификаторов.
Квалификатор – это аннотация, которая используется для идентификации конкретной привязки для типа, когда для этого типа определено несколько привязок.
Рассмотрим пример. Если вам нужно перехватывать вызовы AnalyticsService, вы можете использовать объект OkHttpClient с перехватчиком. Для других сервисов может потребоваться другой способ перехвата вызовов. В этом случае вам нужно указать Hilt, как предоставить две разные реализации OkHttpClient.
Сначала определите квалификаторы, которые вы будете использовать для аннотирования методов @Binds или @Provides:
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class AuthInterceptorOkHttpClient @Qualifier @Retention(AnnotationRetention.BINARY) annotation class OtherInterceptorOkHttpClient
Затем Hilt нужно знать, как предоставить экземпляр типа, соответствующий каждому квалификатору. В этом случае можно использовать модуль Hilt с @Provides.
Оба метода имеют один и тот же тип возвращаемого значения, но квалификаторы помечают их как две разные привязки:
@Module @InstallIn(SingletonComponent::class) object NetworkModule { @AuthInterceptorOkHttpClient @Provides fun provideAuthInterceptorOkHttpClient( authInterceptor: AuthInterceptor ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(authInterceptor) .build() } @OtherInterceptorOkHttpClient @Provides fun provideOtherInterceptorOkHttpClient( otherInterceptor: OtherInterceptor ): OkHttpClient { return OkHttpClient.Builder() .addInterceptor(otherInterceptor) .build() } }
Вы можете внедрить нужный тип, добавив к полю или параметру соответствующий квалификатор:
// As a dependency of another class. @Module @InstallIn(ActivityComponent::class) object AnalyticsModule { @Provides fun provideAnalyticsService( @AuthInterceptorOkHttpClient okHttpClient: OkHttpClient ): AnalyticsService { return Retrofit.Builder() .baseUrl("https://example.com") .client(okHttpClient) .build() .create(AnalyticsService::class.java) } } // As a dependency of a constructor-injected class. class ExampleServiceImpl @Inject constructor( @AuthInterceptorOkHttpClient private val okHttpClient: OkHttpClient ) : ... // At field injection. @AndroidEntryPoint class ExampleActivity: ComponentActivity() { @AuthInterceptorOkHttpClient @Inject lateinit var okHttpClient: OkHttpClient }
Рекомендуется, если вы добавляете квалификатор к типу, добавлять квалификаторы ко всем возможным способам предоставления этой зависимости. Если оставить базовую или общую реализацию без квалификатора, это может привести к ошибкам и внедрению неправильной зависимости.
Стандартные квалификаторы в Hilt
В Hilt есть несколько предопределенных квалификаторов. Например, если вам нужен класс Context из приложения или действия, Hilt предоставляет квалификаторы @ApplicationContext и @ActivityContext.
Предположим, что классу AnalyticsAdapter из примера нужен контекст действия. В следующем примере кода показано, как передать контекст действия в AnalyticsAdapter:
class AnalyticsAdapter @Inject constructor( @ActivityContext private val context: Context, private val service: AnalyticsService ) { ... }
Другие предопределенные привязки, доступные в Hilt, перечислены в разделе Привязки компонентов по умолчанию.
Сгенерированные компоненты для классов Android
Для каждого класса Android, в котором можно выполнить внедрение через поле, есть связанный компонент Hilt, на который можно сослаться в аннотации @InstallIn.
Каждый компонент Hilt отвечает за внедрение своих привязок в соответствующий класс Android.
В предыдущих примерах показано использование ActivityComponent в модулях Hilt.
Hilt предоставляет следующие компоненты:
| Компонент Hilt | Инжектор для |
|---|---|
SingletonComponent |
Application |
ActivityRetainedComponent |
Н/Д |
ViewModelComponent |
ViewModel |
ActivityComponent |
Activity |
ServiceComponent |
Service |
Срок службы компонентов
Hilt автоматически создает и удаляет экземпляры сгенерированных классов компонентов в соответствии с жизненным циклом соответствующих классов Android.
| Сгенерированный компонент | Создано | Удалено |
|---|---|---|
SingletonComponent |
Application#onCreate() |
Application уничтожено |
ActivityRetainedComponent |
Activity#onCreate() |
Activity#onDestroy() |
ViewModelComponent |
Создано: ViewModel |
ViewModel уничтожено |
ActivityComponent |
Activity#onCreate() |
Activity#onDestroy() |
ServiceComponent |
Service#onCreate() |
Service#onDestroy() |
Области действия компонентов
По умолчанию все привязки в Hilt не имеют области действия. Это означает, что каждый раз, когда приложение запрашивает привязку, Hilt создает новый экземпляр нужного типа.
В примере каждый раз, когда Hilt предоставляет AnalyticsAdapter в качестве зависимости другому типу или через внедрение через поле (как в ExampleActivity), Hilt предоставляет новый экземпляр AnalyticsAdapter.
Однако Hilt также позволяет привязать привязку к определенному компоненту. Hilt создает привязку с ограниченной областью действия только один раз для каждого экземпляра компонента, к которому привязана привязка, и все запросы для этой привязки используют один и тот же экземпляр.
В таблице ниже перечислены аннотации области действия для каждого сгенерированного компонента:
| Класс Android | Сгенерированный компонент | Область действия |
|---|---|---|
Application |
SingletonComponent |
@Singleton |
Activity |
ActivityRetainedComponent |
@ActivityRetainedScoped |
ViewModel |
ViewModelComponent |
@ViewModelScoped |
Activity |
ActivityComponent |
@ActivityScoped |
Service |
ServiceComponent |
@ServiceScoped |
В примере, если вы ограничите область действия AnalyticsAdapter до ActivityComponent с помощью @ActivityScoped, Hilt будет предоставлять один и тот же экземпляр AnalyticsAdapter на протяжении всего жизненного цикла соответствующей операции:
@ActivityScoped class AnalyticsAdapter @Inject constructor( private val service: AnalyticsService ) { ... }
Предположим, что у AnalyticsService есть внутреннее состояние, которое требует, чтобы один и тот же экземпляр использовался каждый раз не только в ExampleActivity, но и в любом месте приложения. В этом случае целесообразно ограничить область действия AnalyticsService до SingletonComponent. В результате каждый раз, когда компоненту нужно предоставить экземпляр AnalyticsService, он предоставляет один и тот же экземпляр.
В следующем примере показано, как ограничить привязку компонентом в модуле Hilt. Область действия привязки должна соответствовать области действия компонента, в котором она установлена. Поэтому в этом примере вы должны установить AnalyticsService в SingletonComponent вместо ActivityComponent:
// If AnalyticsService is an interface. @Module @InstallIn(SingletonComponent::class) abstract class AnalyticsModule { @Singleton @Binds abstract fun bindAnalyticsService( analyticsServiceImpl: AnalyticsServiceImpl ): AnalyticsService } // If you don't own AnalyticsService. @Module @InstallIn(SingletonComponent::class) object AnalyticsModule { @Singleton @Provides fun provideAnalyticsService(): AnalyticsService { return Retrofit.Builder() .baseUrl("https://example.com") .build() .create(AnalyticsService::class.java) } }
Подробнее об областях действия компонентов Hilt…
Иерархия компонентов
Установка модуля в компонент позволяет получить доступ к его привязкам как к зависимости других привязок в этом компоненте или в любом дочернем компоненте ниже его в иерархии компонентов.
Привязки по умолчанию для компонентов
Каждый компонент Hilt поставляется с набором привязок по умолчанию, которые Hilt может внедрять в качестве зависимостей в ваши собственные пользовательские привязки. Обратите внимание, что эти привязки соответствуют общему типу действия, а не какому-либо конкретному подклассу. Это связано с тем, что Hilt использует одно определение компонента действия для внедрения всех действий. Для каждого действия используется отдельный экземпляр этого компонента.
| Компонент Android | Привязки по умолчанию |
|---|---|
SingletonComponent |
Application |
ActivityRetainedComponent |
Application |
ViewModelComponent |
SavedStateHandle |
ActivityComponent |
Application, Activity |
ServiceComponent |
Application, Service |
Привязка контекста приложения также доступна в @ApplicationContext.
Пример:
class AnalyticsServiceImpl @Inject constructor( @ApplicationContext context: Context ) : AnalyticsService { ... } // The Application binding is available without qualifiers. class AnalyticsServiceImpl @Inject constructor( application: Application ) : AnalyticsService { ... }
Привязка контекста действия также доступна с помощью @ActivityContext. Пример:
class AnalyticsAdapter @Inject constructor( @ActivityContext context: Context ) { ... } // The Activity binding is available without qualifiers. class AnalyticsAdapter @Inject constructor( activity: ComponentActivity ) { ... }
Внедрение зависимостей в классы, не поддерживаемые Hilt
В Compose стандартный шаблон – внедрить зависимости в @HiltViewModel с помощью внедрения через конструктор, а затем использовать hiltViewModel() внутри composable-функции для доступа к ViewModel. Хотя Hilt поддерживает самые распространенные классы Android, вы все равно можете столкнуться с неподдерживаемыми классами, в которых нужно выполнить внедрение через поле.
В таких случаях вы можете создать точку входа с помощью аннотации @EntryPoint. Точка входа – это граница между кодом, управляемым Hilt, и кодом, который им не управляется. Это точка, в которой код впервые входит в граф объектов, управляемых Hilt. Точки входа позволяют Hilt использовать код, которым Hilt не управляет, для предоставления зависимостей в графе зависимостей.
Например, Hilt не поддерживает поставщиков контента напрямую. Если вы хотите, чтобы поставщик контента использовал Hilt для получения некоторых зависимостей, вам нужно определить интерфейс, аннотированный с помощью @EntryPoint для каждого нужного типа привязки, и включить квалификаторы. Затем добавьте @InstallIn, чтобы указать компонент, в который нужно установить точку входа, как показано ниже:
class ExampleContentProvider : ContentProvider() { @EntryPoint @InstallIn(SingletonComponent::class) interface ExampleContentProviderEntryPoint { fun analyticsService(): AnalyticsService } ... }
Чтобы получить доступ к точке входа, используйте подходящий статический метод из EntryPointAccessors. В качестве параметра следует использовать экземпляр компонента или объект @AndroidEntryPoint, который выступает в качестве держателя компонента. Убедитесь, что компонент, который вы передаете в качестве параметра, и статический метод EntryPointAccessors соответствуют классу Android в аннотации @InstallIn интерфейса @EntryPoint:
class ExampleContentProvider: ContentProvider() { ... override fun query(...): Cursor { val appContext = context?.applicationContext ?: throw IllegalStateException() val hiltEntryPoint = EntryPointAccessors.fromApplication(appContext, ExampleContentProviderEntryPoint::class.java) val analyticsService = hiltEntryPoint.analyticsService() ... } }
В этом примере для получения точки входа необходимо использовать ApplicationContext, поскольку точка входа установлена в SingletonComponent. Если бы нужная привязка находилась в ActivityComponent, вы бы использовали ActivityContext.
Hilt и Dagger
Hilt – это официально рекомендованная библиотека для внедрения зависимостей в Android. Она предоставляет стандартизированный, обоснованный и эффективный способ внедрения зависимостей в приложение, специально оптимизированный для Jetpack Compose и архитектур с одним действием.
Цели Hilt:
- Создать стандартный набор компонентов и областей действия, чтобы упростить настройку, чтение и обмен кодом между приложениями.
- Чтобы упростить предоставление разных привязок для разных типов сборки, например тестирования, отладки или выпуска.
Поскольку операционная система Android создает экземпляры многих собственных классов фреймворка, использование Dagger в приложении Android требует написания значительного количества шаблонного кода. Hilt сокращает объем шаблонного кода, который используется при работе с Dagger в приложении для Android. Hilt автоматически генерирует и предоставляет следующее:
- Компоненты для интеграции классов фреймворка Android с Dagger, которые в противном случае вам пришлось бы создавать вручную.
- Аннотации области действия для использования с компонентами, которые Hilt генерирует автоматически.
- Предопределенные привязки для представления классов Android, таких как
ApplicationилиActivity. - Стандартные модификаторы, представляющие
@ApplicationContextи@ActivityContext.
Код Dagger и Hilt может сосуществовать в одной базе кода. Однако в большинстве случаев лучше использовать Hilt для управления всеми вашими использованиями Dagger на Android. Чтобы перенести проект, в котором используется Dagger, в Hilt, ознакомьтесь с руководством по переносу.
Дополнительные ресурсы
Дополнительную информацию о Hilt можно найти по ссылкам ниже.
Образцы
Блоги
- Внедрение зависимостей в Android с помощью Hilt
- Области видимости в Android и Hilt
- Добавление компонентов в иерархию Hilt
- Как перенести приложение Google I/O в Hilt