Anvil для Kotlin: полное руководство с примерами
1. Что это и зачем нужна
Anvil — это библиотека кодогенерации для Kotlin, которая работает поверх фреймворков внедрения зависимостей (DI) Dagger и Hilt. Её основная цель — устранить рутинную работу по написанию boilerplate-кода, связанного с модулями DI, компонентами, фабриками и точками входа. Вместо того чтобы вручную создавать и аннотировать модули Dagger, вы просто помечаете классы аннотациями Anvil, и библиотека генерирует весь необходимый код на этапе компиляции.
Зачем это нужно? В крупных проектах на Android или Kotlin Multiplatform количество классов, которые нужно зарегистрировать в графе зависимостей, может исчисляться сотнями. Каждый раз добавлять новый модуль Dagger, прописывать @Provides или @Binds — это медленно, подвержено ошибкам и усложняет рефакторинг. Anvil решает эту проблему, автоматизируя создание модулей на основе простых аннотаций. Это особенно полезно в архитектуре, основанной на фича-модулях (feature modules), где каждый модуль должен быть независимо собран и протестирован.
Anvil тесно интегрируется с Gradle и использует Kotlin Symbol Processing (KSP) или KAPT для генерации кода. Она поддерживает как Dagger, так и Hilt, и может работать в мультиплатформенных проектах. Библиотека разработана компанией Square, известной своими инструментами для Android-разработки (Retrofit, OkHttp, Moshi).
2. Установка
Для установки Anvil в ваш проект Kotlin/Gradle добавьте следующие зависимости в файл build.gradle.kts (корневой уровень проекта):
// В корневом build.gradle.kts
plugins {
id("com.google.dagger.hilt.android") version "2.48" apply false
id("com.squareup.anvil") version "2.4.7" apply false
}
Затем в модуле приложения (например, app/build.gradle.kts):
plugins {
id("com.android.application")
kotlin("android")
id("com.google.dagger.hilt.android")
id("com.squareup.anvil")
}
android {
// ... ваша конфигурация Android
}
dependencies {
// Hilt
implementation("com.google.dagger:hilt-android:2.48")
kapt("com.google.dagger:hilt-compiler:2.48")
// Anvil
implementation("com.squareup.anvil:annotations:2.4.7")
ksp("com.squareup.anvil:compiler:2.4.7") // или kapt, если не используете KSP
}
Если вы используете KSP вместо KAPT, убедитесь, что плагин KSP подключен:
plugins {
id("com.google.devtools.ksp") version "1.9.0-1.0.13"
}
После синхронизации Gradle Anvil готов к работе.
3. Быстрый старт — минимальный рабочий пример
Рассмотрим простейший пример: у нас есть класс UserRepository, который нужно внедрить в MainViewModel.
Шаг 1. Создайте интерфейс или класс, который будет внедряться:
// UserRepository.kt
class UserRepository @Inject constructor() {
fun getUser(): String = "John Doe"
}
Шаг 2. Создайте класс, который будет использовать зависимость:
// MainViewModel.kt
class MainViewModel @Inject constructor(
private val userRepository: UserRepository
) {
fun getUserName(): String = userRepository.getUser()
}
Шаг 3. Создайте компонент Hilt (или Dagger) с помощью Anvil:
// AppComponent.kt
@Singleton
@Component
@MergeComponent(AppScope::class) // Аннотация Anvil
interface AppComponent {
fun inject(app: MyApplication)
}
// AppScope.kt (определяем скоуп)
@Scope
@MustBeDocumented
annotation class AppScope
Шаг 4. Зарегистрируйте классы в графе зависимостей с помощью аннотаций Anvil:
// UserRepository.kt (добавляем аннотацию)
@ContributesBinding(AppScope::class)
class UserRepository @Inject constructor() {
fun getUser(): String = "John Doe"
}
// MainViewModel.kt (добавляем аннотацию)
@ContributesBinding(AppScope::class)
class MainViewModel @Inject constructor(
private val userRepository: UserRepository
) {
fun getUserName(): String = userRepository.getUser()
}
Шаг 5. Используйте в Activity:
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
@Inject lateinit var viewModel: MainViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
println(viewModel.getUserName()) // Выведет "John Doe"
}
}
Anvil сгенерирует все необходимые модули Dagger/Hilt автоматически.
4. Основные методы/функции/классы
4.1. @ContributesBinding
Сигнатура: @Target(CLASS) @Retention(SOURCE) annotation class ContributesBinding(vararg val scope: KClass<*>)
Что делает: Указывает, что класс должен быть добавлен в граф зависимостей как binding (привязка) для указанного скоупа. Anvil сгенерирует модуль Dagger с @Binds или @Provides.
Пример:
@ContributesBinding(AppScope::class)
class MyRepository @Inject constructor() : Repository {
override fun fetch(): String = "Data"
}
4.2. @ContributesMultibinding
Сигнатура: @Target(CLASS) @Retention(SOURCE) annotation class ContributesMultibinding(vararg val scope: KClass<*>)
Что делает: Регистрирует класс как мультибиндинг (например, для Set или Map). Полезно для стратегий, плагинов или обработчиков.
Пример:
@ContributesMultibinding(AppScope::class)
class EmailNotifier @Inject constructor() : Notifier {
override fun send(message: String) { /* отправка email */ }
}
@ContributesMultibinding(AppScope::class)
class SmsNotifier @Inject constructor() : Notifier {
override fun send(message: String) { /* отправка SMS */ }
}
// Внедрение:
class NotificationService @Inject constructor(
private val notifiers: Set<@JvmSuppressWildcards Notifier>
) { ... }
4.3. @MergeComponent
Сигнатура: @Target(CLASS) @Retention(SOURCE) annotation class MergeComponent(vararg val scope: KClass<*>)
Что делает: Аннотирует компонент Dagger/Hilt, указывая, что Anvil должен объединить в него все привязки из указанных скоупов. Заменяет ручное перечисление модулей.
Пример:
@Singleton
@Component
@MergeComponent(AppScope::class, FeatureScope::class)
interface AppComponent {
fun inject(app: MyApplication)
}
4.4. @MergeSubcomponent
Сигнатура: @Target(CLASS) @Retention(SOURCE) annotation class MergeSubcomponent(vararg val scope: KClass<*>)
Что делает: Аналог @MergeComponent, но для подкомпонентов. Используется, когда нужно создать вложенный граф зависимостей (например, для фича-модуля).
Пример:
@FeatureScope
@Subcomponent
@MergeSubcomponent(FeatureScope::class)
interface FeatureComponent {
fun inject(featureActivity: FeatureActivity)
}
4.5. @ContributesTo
Сигнатура: @Target(CLASS) @Retention(SOURCE) annotation class ContributesTo(vararg val scope: KClass<*>)
Что делает: Указывает, что объект (обычно модуль) должен быть добавлен в компонент указанного скоупа. Это более низкоуровневая альтернатива @ContributesBinding.
Пример:
@Module
@ContributesTo(AppScope::class)
object NetworkModule {
@Provides @Singleton
fun provideOkHttp(): OkHttpClient = OkHttpClient.Builder().build()
}
4.6. @SingleIn
Сигнатура: @Target(CLASS) @Retention(SOURCE) annotation class SingleIn(val scope: KClass<*>)
Что делает: Указывает, что класс должен быть синглтоном в рамках указанного скоупа. Anvil сгенерирует соответствующий @Provides @Singleton.
Пример:
@SingleIn(AppScope::class)
class DatabaseHelper @Inject constructor() {
// будет синглтоном для всего приложения
}
4.7. @ForScope
Сигнатура: @Target(ANNOTATION_CLASS) @Retention(SOURCE) annotation class ForScope(val scope: KClass<*>)
Что делает: Используется для создания квалификаторов (qualifiers), привязанных к скоупу. Позволяет различать зависимости одного типа в разных скоупах.
Пример:
@Qualifier
@ForScope(AppScope::class)
annotation class AppScopeQualifier
@Qualifier
@ForScope(FeatureScope::class)
annotation class FeatureScopeQualifier
// Использование:
class MyClass @Inject constructor(
@AppScopeQualifier private val config: AppConfig,
@FeatureScopeQualifier private val config: FeatureConfig
)
4.8. @ContributesSubcomponent
Сигнатура: @Target(CLASS) @Retention(SOURCE) annotation class ContributesSubcomponent(vararg val scope: KClass<*>)
Что делает: Регистрирует фабрику подкомпонента. Позволяет Anvil автоматически создавать подкомпоненты и связывать их с родительским компонентом.
Пример:
@ContributesSubcomponent(FeatureScope::class)
interface FeatureComponent {
fun inject(activity: FeatureActivity)
@ContributesSubcomponent.Factory
interface Factory {
fun create(): FeatureComponent
}
}
5. Реальный практический пример (100-200 строк кода)
Рассмотрим реальный сценарий: приложение для заметок с фича-модулями. У нас есть общий модуль core и модуль notes.
Структура проекта:
app/
├── core/
│ ├── src/main/kotlin/com/example/core/
│ │ ├── di/
│ │ │ ├── AppScope.kt
│ │ │ └── CoreComponent.kt
│ │ ├── data/
│ │ │ ├── Database.kt
│ │ │ └── NoteRepository.kt
│ │ └── domain/
│ │ └── Note.kt
├── notes/
│ ├── src/main/kotlin/com/example/notes/
│ │ ├── di/
│ │ │ ├── FeatureScope.kt
│ │ │ └── NotesComponent.kt
│ │ ├── ui/
│ │ │ ├── NotesActivity.kt
│ │ │ └── NotesViewModel.kt
│ │ └── data/
│ │ └── NotesApi.kt
Файл: core/src/.../AppScope.kt
package com.example.core.di
import javax.inject.Scope
@Scope
@MustBeDocumented
annotation class AppScope
Файл: core/src/.../CoreComponent.kt
package com.example.core.di
import dagger.Component
import com.squareup.anvil.annotations.MergeComponent
import javax.inject.Singleton
@Singleton
@Component
@MergeComponent(AppScope::class)
interface CoreComponent {
fun inject(app: MyApplication)
}
Файл: core/src/.../Database.kt
package com.example.core.data
import com.squareup.anvil.annotations.ContributesBinding
import com.example.core.di.AppScope
import javax.inject.Inject
import javax.inject.Singleton
@ContributesBinding(AppScope::class)
@Singleton
class Database @Inject constructor() {
fun query(sql: String): List
Файл: core/src/.../NoteRepository.kt
package com.example.core.data
import com.squareup.anvil.annotations.ContributesBinding
import com.example.core.di.AppScope
import javax.inject.Inject
import javax.inject.Singleton
@ContributesBinding(AppScope::class)
@Singleton
class NoteRepository @Inject constructor(
private val database: Database
) {
fun getAllNotes(): List {
val rows = database.query("SELECT * FROM notes")
return rows.map { Note(it["id"] as Int, it["title"] as String) }
}
}
Файл: notes/src/.../FeatureScope.kt
package com.example.notes.di
import javax.inject.Scope
@Scope
@MustBeDocumented
annotation class FeatureScope
Файл: notes/src/.../NotesComponent.kt
package com.example.notes.di
import dagger.Subcomponent
import com.s Также в библиотеке
Конфигурация приложений PHP: библиотеки и инструменты для гибкой настройки
Конфигурация приложений Kotlin: Библиотека Hoplite