Внедрение зависимостей (DI) спрашивают и как принцип, и как знание конкретного инструмента. В российских Android-командах встречаются Hilt (Dagger), Koin и Dagger 2 в чистом виде в больших легаси-проектах.
Зачем DI
Класс не создаёт свои зависимости сам, а получает их снаружи, обычно через конструктор:
class ProfileRepository(
private val api: ProfileApi,
private val dao: ProfileDao,
)
Плюсы: в тестах легко подменить api фейком, зависимости видны в сигнатуре, объекты можно переиспользовать (один OkHttpClient на приложение), время жизни объектов управляется централизованно. Ручной DI (фабрики и контейнер в Application) тоже DI, библиотека лишь автоматизирует граф.
Service Locator (класс сам просит зависимость у реестра: get<Api>()) похож, но зависимости скрыты внутри класса. Koin формально ближе к service locator, хотя при внедрении в конструктор выглядит как DI.
Hilt
Hilt построен на Dagger: граф проверяется и генерируется при компиляции (KSP или kapt), ошибки «нет биндинга» видны до запуска, рантайм быстрый.
@HiltAndroidApp
class App : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity()
@HiltViewModel
class ProfileViewModel @Inject constructor(
private val repo: ProfileRepository,
private val savedState: SavedStateHandle,
) : ViewModel()
class ProfileRepository @Inject constructor(private val api: ProfileApi)
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides @Singleton
fun okHttp(): OkHttpClient = OkHttpClient.Builder().build()
@Provides @Singleton
fun api(client: OkHttpClient): ProfileApi =
Retrofit.Builder().baseUrl(BASE_URL).client(client)
.addConverterFactory(json.asConverterFactory("application/json".toMediaType()))
.build().create(ProfileApi::class.java)
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepoModule {
@Binds abstract fun bindRepo(impl: ProfileRepositoryImpl): ProfileRepository
}
@Inject constructorговорит Hilt, как создать класс.@Providesдля объектов, которые нельзя пометить@Inject(сторонние классы, билдеры).@Bindsсвязывает интерфейс с реализацией, генерирует меньше кода, чем@Provides.@Qualifier(@Named) различает два биндинга одного типа.
Компоненты и скоупы Hilt
| Компонент | Скоуп | Живёт |
|---|---|---|
SingletonComponent | @Singleton | всё время приложения |
ActivityRetainedComponent | @ActivityRetainedScoped | переживает поворот |
ViewModelComponent | @ViewModelScoped | время жизни ViewModel |
ActivityComponent | @ActivityScoped | экземпляр Activity |
FragmentComponent | @FragmentScoped | экземпляр фрагмента |
ServiceComponent | @ServiceScoped | сервис |
Без аннотации скоупа Hilt создаёт новый экземпляр при каждом внедрении. Скоуп делает объект единственным в пределах компонента. Не нужно делать всё @Singleton: объект будет жить вечно и держать память.
Ещё полезное: @ApplicationContext для контекста приложения, @AssistedInject для параметров, известных только в рантайме (например, id для WorkManager-воркера через @HiltWorker), hiltViewModel() в Compose, @EntryPoint для классов, которые Hilt не создаёт сам.
Koin
val appModule = module {
single { OkHttpClient() }
single<ProfileRepository> { ProfileRepositoryImpl(get()) }
factory { FormatPrice(get()) }
viewModel { ProfileViewModel(get(), get()) } // или viewModelOf(::ProfileViewModel)
}
startKoin { androidContext(this@App); modules(appModule) }
Koin это DSL на Kotlin без кодогенерации: граф разрешается в рантайме. Плюсы: просто, быстро собирается, работает в Kotlin Multiplatform. Минусы: ошибка «нет определения» проявится только при запуске (есть verify()/checkModules в тестах и Koin Annotations с проверкой при компиляции), разрешение в рантайме чуть медленнее.
Hilt или Koin
- Hilt: большие Android-проекты, проверка графа при компиляции, стандарт Google, интеграция с ViewModel, WorkManager, Navigation. Минус: время сборки и порог входа.
- Koin: небольшие и средние проекты, KMP, быстрый старт. Минус: ошибки в рантайме.
Частые ошибки
- Всё помечено
@Singleton, и объекты держат память и состояние между экранами. - Внедрение Activity-контекста в синглтон: утечка.
@Providesтам, где хватает@Inject constructorили@Binds.- Ручное создание зависимостей внутри класса, обходящее DI, и невозможность подменить их в тестах.
Что спросят на собесе
- Что такое DI и чем он отличается от Service Locator?
- Как Hilt создаёт граф и почему ошибки видны при компиляции?
@Providesпротив@Binds.- Какие компоненты и скоупы есть в Hilt? Что будет без скоупа?
- Чем Koin отличается от Hilt?
- Как подменить зависимость в тесте?