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

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

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

Начать курс

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> = emptyList()
}

Файл: 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                    

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