Введение

«Расскажите, как у вас устроен DI?» — на собеседовании на middle этот вопрос прозвучит почти гарантированно. И если проект ваш на Hilt, от ответа «ну, поставили аннотации и всё заработало» до уверенного разбора графа зависимостей — дистанция в несколько месяцев практики. Разберём Hilt так, чтобы завтра вы уже могли объяснять его архитектуру.

Внедрение зависимостей в Android — задача особая: Activity, Fragment и Service создаёт система, поэтому передать им зависимости через конструктор вы не можете. DI-фреймворк строит граф объектов вне UI-компонентов, а в саму Activity приходят уже готовые зависимости.

Hilt — это обёртка над Dagger от Google, которая берёт на себя всю рутинную настройку: генерирует Dagger-компоненты и код инжекции при компиляции, а вам остаётся только описать, как создавать объекты и куда их внедрять. Это официальный инструмент, который рекомендует сама документация Dagger, и именно его вы увидите в большинстве современных проектов.

Что такое Hilt и зачем он нужен

Главное, что нужно понимать про Hilt с самого начала: это не отдельный DI-фреймворк, а надстройка над Dagger с автопилотом. Под капотом — те же компоненты, аннотации и кодогенерация, но вам не нужно вручную описывать @Component, связывать модули и писать код инжекции.

Что Hilt делает за вас:

  • генерирует стандартный набор компонентов (SingletonComponent, ActivityComponent, FragmentComponent и другие) на основе классовой структуры приложения;
  • автоматически внедряет зависимости в Activity, Fragment, View, Service и BroadcastReceiver;
  • позволяет подменять привязки для разных build-вариантов (debug, release, тесты);
  • даёт готовую интеграцию с ViewModel, WorkManager и Compose.

Всё это происходит на этапе компиляции: если граф зависимостей собран неправильно, вы узнаете об этом при сборке, а не в рантайме на устройстве пользователя. Это главное преимущество Hilt над DI-контейнерами, которые работают во время выполнения.

Почему Hilt, а не ручной DI

Пока проект маленький, ручная передача зависимостей через конструктор работает отлично. Но когда классов становится несколько десятков, «ад конструкторов» съедает время: вы вручную собираете граф, забываете обновить одно из мест создания — и получаете NPE в рантайме. Hilt собирает граф за вас и проверяет его при компиляции.

Подключение Hilt: первые шаги

Начинается всё с подключения плагина и зависимостей. В корневом build.gradle.kts подключается Hilt Gradle plugin, в модуле app — плагин и библиотека:

build.gradle.kts
plugins {
    id("com.google.dagger.hilt.android") version "2.56.2" apply false
}

dependencies {
    implementation("com.google.dagger:hilt-android:2.56.2")
    kapt("com.google.dagger:hilt-compiler:2.56.2")
}

Затем три обязательных шага, которые проверяют на собеседовании:

1. @HiltAndroidApp на классе Application. Это обязательное условие: без него Hilt не запустится. Аннотация запускает генерацию всех компонентов и создаёт базовый класс приложения, в котором происходит инжекция:

App.kt
@HiltAndroidApp
class App : Application()

2. @AndroidEntryPoint на Android-классах. Аннотация включает инжекцию в Activity, Fragment, View, Service и BroadcastReceiver. Важный нюанс: Activity должна наследоваться от ComponentActivity, а Fragment — от androidx-версии androidx.fragment.app.Fragment.

MainActivity.kt
@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    @Inject
    lateinit var analytics: Analytics

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // инжекция уже произошла — analytics можно использовать
    }
}

3. @Inject constructor на классах, которые Hilt должен создавать сам. Так фреймворк узнаёт, как построить экземпляр со всеми его зависимостями:

NotesRepository.kt
class NotesRepository @Inject constructor(
    private val api: ApiService
) {
    fun loadNotes(): List<Note> = api.getNotes()
}

Ошибка, которую я регулярно вижу на собеседованиях: кандидат путает два механизма — @Inject constructor говорит Hilt, как создавать класс, а поле с @Inject в Activity — куда внедрять готовый объект. Это разные вещи, и смешивать их в ответе — верный способ показать, что тема выучена поверхностно.

Модули: @Module, @Provides, @InstallIn

Через конструктор можно создавать только свои классы. А как быть с Retrofit, OkHttp, Room — библиотеками, чьи объекты вы не контролируете? Для этого существуют модули.

Модуль — это класс с аннотацией @Module, а @InstallIn указывает, в какой Hilt-компонент его установить. Если модуль забыть пометить @InstallIn, он просто не попадёт в граф — и сборка упадёт с ошибкой.

NetworkModule.kt
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {

    @Provides
    @Singleton
    fun provideOkHttpClient(): OkHttpClient = OkHttpClient.Builder().build()

    @Provides
    @Singleton
    fun provideRetrofit(client: OkHttpClient): Retrofit = Retrofit.Builder()
        .baseUrl("https://api.example.com/")
        .client(client)
        .build()
}

Что здесь происходит:

  • @Provides — аннотация над функцией, которая создаёт объект. Hilt вызывает её, когда в графе нужна зависимость соответствующего типа.
  • @Singleton — скоуп: провайдер вызовется один раз, а результат переживёт всё приложение.
  • Функция provideRetrofit принимает OkHttpClient — Hilt сам найдёт его в графе и передаст. Это обычная зависимость между провайдерами.

Для интерфейсов используется @Binds: если у вас есть interface NotesRepository и реализация NotesRepositoryImpl, модуль с абстрактной функцией свяжет интерфейс с реализацией.

Иерархия компонентов

Компоненты Hilt вложены друг в друга: SingletonComponent → ActivityRetainedComponent → ActivityComponent → FragmentComponent. Модуль, установленный в SingletonComponent, доступен всем «младшим» компонентам, а вот наоборот — нет. Например, из SingletonComponent нельзя получить объект, привязанный к Activity. Это осознанный дизайн, о котором стоит рассказать на собеседовании — детали в документации.

Скоупы и Hilt ViewModel

Скоуп определяет время жизни экземпляра. Это любимый вопрос интервьюеров, потому что он проверяет понимание жизненного цикла приложения, а не знание синтаксиса.

Основные скоупы Hilt:

  • @Singleton — один экземпляр на всё приложение: Retrofit, база данных, аналитика.
  • @ActivityRetainedScoped — переживает поворот экрана, умирает вместе с Activity.
  • @ViewModelScoped — один экземпляр на один ViewModel.
  • @ActivityScoped — живёт, пока жива Activity.
  • @FragmentScoped — на время жизни Fragment.

Правило простое: зависимость должна жить не дольше того, кто её использует. Retrofit — Singleton, состояние экрана — в ViewModel, а вот вешать @Singleton на репозиторий, который хранит данные конкретного экрана, — классическая ошибка, ведущая к утечкам и рассинхрону данных.

ViewModel в Hilt подключается через @HiltViewModel — фреймворк сам создаст фабрику и прокинут все зависимости:

NotesViewModel.kt
@HiltViewModel
class NotesViewModel @Inject constructor(
    private val repository: NotesRepository
) : ViewModel() {

    val notes: Flow<List<Note>> = repository.loadNotes()
}

Теперь в Activity или Compose-экране вы получаете ViewModel привычным способом — viewModel() или by viewModels(), без ручной фабрики.

  • Забыли @InstallIn — модуль не попадает в граф, сборка падает.
  • Скоуп «на всякий случай» — @Singleton везде подряд; в итоге у экранов общий мутируемый стейт и утечки.
  • @AndroidEntryPoint на обычном классе — аннотация работает только с Android-компонентами, для остального есть @EntryPoint или конструкторная инжекция.
  • Инжекция в не-ViewModel классы через by viewModels() — Hilt умеет создавать только ViewModel-классы с @HiltViewModel.
  • Непонимание, зачем @Inject constructor — если кандидат не может объяснить, как Hilt узнаёт способ создания класса, тема не выучена.

Пара слов обо мне: я Рустем Бикбулатов, senior Android-разработчик и ментор Яндекс Практикума. Провожу технические собеседования, за плечами более 100 разборов кандидатов. Чаще всего помогаю разработчикам с опытом 1–2 года закрыть пробелы в Kotlin, Coroutines и архитектуре и перейти на middle за 4–8 недель — в том числе и по теме DI.

Нужна помощь в переходе на middle?

Бесплатная диагностика: разберу ваши пробелы в DI и архитектуре и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.

Итоги

Hilt — стандарт внедрения зависимостей в современном Android, и понимать его нужно не на уровне «поставили и работает», а на уровне устройства графа.

  • Суть: Hilt — обёртка над Dagger с кодогенерацией при компиляции: ошибки в графе ловятся на сборке, а не в рантайме.
  • Три кита: @HiltAndroidApp на Application, @AndroidEntryPoint на Android-компонентах, @Inject constructor на создаваемых классах.
  • Модули и скоупы: @Module + @InstallIn описывают создание внешних зависимостей, а скоуп определяет время жизни экземпляра — от Singleton до FragmentScoped.

Главное, что нужно запомнить

Отвечая на вопрос про Hilt, начинайте с проблемы: Activity и Fragment создаёт система, поэтому DI-контейнер обязателен. Затем — кодогенерация при компиляции и стандартные компоненты. Такой ответ сразу отделяет вас от кандидатов, которые выучили аннотации, но не понимают, зачем всё это нужно.

Если на этом месте поймали себя на мысли «вроде всё понятно, но в своём проекте я бы запутался» — это нормально, так работает большинство. Напишите в Telegram, разберём вашу ситуацию и составим план, что закрыть в первую очередь.