Hilt/Dagger для Kotlin: полное руководство с примерами

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в Dependency Injection и библиотеки Hilt/Dagger

В современной разработке приложений на Kotlin (особенно для Android) одной из ключевых архитектурных концепций является Dependency Injection (DI) — внедрение зависимостей. Это паттерн проектирования, при котором объект получает свои зависимости извне, а не создает их самостоятельно. Это делает код более гибким, тестируемым и поддерживаемым.

Dagger — это мощный фреймворк для DI, разработанный Google, который генерирует код на этапе компиляции. Он гарантирует, что все зависимости будут предоставлены корректно, и исключает ошибки времени выполнения, связанные с отсутствием зависимостей. Однако Dagger имеет крутую кривую обучения из-за своей сложности.

Hilt — это надстройка над Dagger, созданная для упрощения его использования в Android-приложениях. Hilt предоставляет стандартные компоненты, скоуп-аннотации и автоматическую интеграцию с жизненным циклом Android (Activity, Fragment, ViewModel и т.д.). Если вы пишете на Kotlin для Android, Hilt — это рекомендуемый выбор, так как он значительно снижает boilerplate-код.

В этой статье мы подробно рассмотрим обе библиотеки, но основной фокус сделаем на Hilt, как на более современном и удобном инструменте. Вы узнаете, как установить Hilt, как использовать его основные аннотации и методы, а также разберем реальные примеры и типичные ошибки.

Установка Hilt в проект Kotlin/Android

Hilt не устанавливается через pip или npm. Он интегрируется в проект Gradle. Для Kotlin и Android процесс выглядит следующим образом:

1. Добавление плагина Hilt в корневой build.gradle (Project-level)

// build.gradle (Project)
buildscript {
    repositories {
        google()
        mavenCentral()
    }
    dependencies {
        // ... другие плагины
        classpath 'com.google.dagger:hilt-android-gradle-plugin:2.50' // Проверьте актуальную версию
    }
}

2. Применение плагина в build.gradle модуля приложения (Module-level)

// build.gradle (Module: app)
plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'kotlin-kapt' // Обязательно для Hilt
    id 'dagger.hilt.android.plugin'
}

android {
    // ... ваша конфигурация
}

dependencies {
    implementation 'com.google.dagger:hilt-android:2.50'
    kapt 'com.google.dagger:hilt-compiler:2.50'

    // Для ViewModel (опционально, но рекомендуется)
    implementation 'androidx.hilt:hilt-navigation-compose:1.1.0' // Для Jetpack Compose
    // или
    implementation 'androidx.hilt:hilt-lifecycle-viewmodel:1.0.0-alpha03' // Для старых версий
}

3. Настройка Application класса

Каждое приложение, использующее Hilt, должно иметь класс Application, аннотированный @HiltAndroidApp.

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApplication : Application() {
    override fun onCreate() {
        super.onCreate()
        // Инициализация приложения
    }
}

Не забудьте указать этот класс в AndroidManifest.xml:

<application
    android:name=".MyApplication"
    ... >

Быстрый старт с примером кода

Давайте создадим простой пример: приложение, которое приветствует пользователя, используя Hilt для внедрения зависимости Greeter.

Шаг 1: Создайте класс, который будет внедряться

import javax.inject.Inject

class Greeter @Inject constructor() {
    fun greet(name: String): String {
        return "Hello, $name!"
    }
}

Аннотация @Inject на конструкторе говорит Hilt, как создавать экземпляр этого класса.

Шаг 2: Внедрение в Activity

import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import dagger.hilt.android.AndroidEntryPoint
import javax.inject.Inject

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    @Inject
    lateinit var greeter: Greeter // Hilt внедрит сюда экземпляр Greeter

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        val textView: TextView = findViewById(R.id.textView)
        textView.text = greeter.greet("PythonLib")
    }
}

Аннотация @AndroidEntryPoint регистрирует Activity как компонент, в который Hilt может внедрять зависимости.

Основные методы/аннотации Hilt/Dagger (минимум 10)

В Hilt и Dagger основная работа строится не на функциях, а на аннотациях. Каждая аннотация — это "метод" конфигурации DI. Рассмотрим ключевые из них.

1. @Inject

Что делает: Указывает Hilt, как создать экземпляр класса. Может использоваться на конструкторе, поле или методе.

Параметры: Не принимает параметров.

Пример:

class DatabaseHelper @Inject constructor() {
    fun query() { /* ... */ }
}

class UserRepository {
    @Inject
    lateinit var dbHelper: DatabaseHelper // Внедрение через поле
}

2. @Module

Что делает: Помечает класс как модуль, который предоставляет зависимости. Модули — это фабрики, которые создают объекты, если у них нет аннотированного конструктора (например, сторонние библиотеки).

Параметры: Не принимает параметров.

Пример:

import dagger.Module
import dagger.Provides

@Module
class NetworkModule {
    // ...
}

3. @Provides

Что делает: Используется внутри @Module для определения метода, который создает и возвращает зависимость.

Параметры: Метод может принимать другие зависимости (которые будут внедрены Hilt).

Пример:

@Module
class NetworkModule {
    @Provides
    fun provideRetrofit(): Retrofit {
        return Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .build()
    }
}

4. @Singleton

Что делает: Гарантирует, что в рамках одного компонента (например, всего приложения) будет существовать только один экземпляр зависимости.

Параметры: Не принимает.

Пример:

@Module
object AppModule {
    @Provides
    @Singleton
    fun provideDatabase(): Database {
        return Database.create()
    }
}

5. @Binds

Что делает: Используется в абстрактных модулях для привязки интерфейса к его реализации. Это более эффективно, чем @Provides, так как Hilt не генерирует лишний код.

Параметры: Метод должен принимать один параметр — реализацию интерфейса.

Пример:

interface Repository {
    fun getData(): String
}

class RepositoryImpl @Inject constructor() : Repository {
    override fun getData() = "Data from repository"
}

@Module
abstract class RepositoryModule {
    @Binds
    abstract fun bindRepository(impl: RepositoryImpl): Repository
}

6. @Named

Что делает: Позволяет различать несколько зависимостей одного типа. Например, две строки (URL и API ключ) или два экземпляра Retrofit с разными конфигурациями.

Параметры: Принимает строку — имя квалификатора.

Пример:

@Module
class ConfigModule {
    @Provides
    @Named("baseUrl")
    fun provideBaseUrl(): String = "https://api.example.com"

    @Provides
    @Named("apiKey")
    fun provideApiKey(): String = "12345-abcde"
}

// Внедрение
class ApiClient @Inject constructor(
    @Named("baseUrl") private val baseUrl: String,
    @Named("apiKey") private val apiKey: String
) { /* ... */ }

7. @Qualifier

Что делает: Более типобезопасная альтернатива @Named. Позволяет создавать свои собственные аннотации-квалификаторы.

Параметры: Создается как обычная аннотация.

Пример:

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class BaseUrl

@Qualifier
@Retention(AnnotationRetention.BINARY)
annotation class ApiKey

@Module
object ConfigModule {
    @BaseUrl
    @Provides
    fun provideBaseUrl(): String = "https://api.example.com"

    @ApiKey
    @Provides
    fun provideApiKey(): String = "12345-abcde"
}

// Внедрение
class ApiClient @Inject constructor(
    @BaseUrl private val baseUrl: String,
    @ApiKey private val apiKey: String
) { /* ... */ }

8. @HiltAndroidApp

Что делает: Обязательная аннотация для базового класса Application. Инициализирует Hilt и генерирует базовый компонент.

Параметры: Не принимает.

Пример: (см. раздел "Установка")

9. @AndroidEntryPoint

Что делает: Регистрирует Activity, Fragment, View, Service или BroadcastReceiver как цель для внедрения зависимостей.

Параметры: Не принимает.

Пример:

@AndroidEntryPoint
class MyFragment : Fragment() {
    @Inject
    lateinit var viewModelFactory: ViewModelFactory
    // ...
}

10. @HiltViewModel

Что делает: Специальная аннотация для ViewModel в Android. Позволяет Hilt автоматически создавать ViewModel и внедрять в нее зависимости.

Параметры: Не принимает.

Пример:

import androidx.lifecycle.ViewModel
import dagger.hilt.android.lifecycle.HiltViewModel
import javax.inject.Inject

@HiltViewModel
class MyViewModel @Inject constructor(
    private val repository: Repository
) : ViewModel() {
    // Логика ViewModel
}

11. @EntryPoint

Что делает: Используется для внедрения зависимостей в классы, которые не поддерживаются Hilt напрямую (например, обычные классы Kotlin, ContentProvider). Создает точку входа в граф зависимостей.

Параметры: Не принимает.

Пример:

@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
    fun getGreeter(): Greeter
}

// Использование
val entryPoint = EntryPointAccessors.fromApplication(
    context,
    MyEntryPoint::class.java
)
val greeter = entryPoint.getGreeter()

Реальный практический пример использования

Представим, что мы пишем приложение для просмотра погоды. У нас есть WeatherRepository, который использует WeatherApi (Retrofit) и LocationProvider. Мы хотим, чтобы все зависимости внедрялись автоматически.

Структура проекта:

  • WeatherApi.kt — интерфейс Retrofit
  • WeatherRepository.kt — репозиторий
  • MainViewModel.kt — ViewModel
  • MainActivity.kt — Activity
  • NetworkModule.kt — модуль для Retrofit

Код:

// 1. WeatherApi.kt
import retrofit2.http.GET
import retrofit2.http.Query

interface WeatherApi {
    @GET("weather")
    suspend fun getWeather(@Query("q") city: String): WeatherResponse
}

// 2. NetworkModule.kt
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import retrofit2.Retro                    

Рекомендаций