Введение в 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— интерфейс RetrofitWeatherRepository.kt— репозиторийMainViewModel.kt— ViewModelMainActivity.kt— ActivityNetworkModule.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