DI спрашивают на всех мобильных собесах: что это, чем отличается от Service Locator, как устроены скоупы и какой инструмент вы используете.
Принцип
Объект получает зависимости снаружи, а не создаёт их сам:
// плохо
final class ProfileViewModel {
private let repo = ProfileRepository(api: APIClient.shared)
}
// хорошо
final class ProfileViewModel {
private let repo: ProfileRepositoryProtocol
init(repo: ProfileRepositoryProtocol) { self.repo = repo }
}
Способы: через конструктор (предпочтительно: зависимости явные и обязательные), через свойство (когда объект создаёт система, например Activity), через параметр метода.
Главная выгода: тестируемость. В тесте view model получает фейковый репозиторий, и проверка идёт без сети и базы.
Composition root
Граф объектов собирается в одном месте у входа в приложение (или модуля): создаются клиент, репозитории, фабрики экранов. Остальной код получает готовые зависимости.
Service Locator
val api = ServiceLocator.get<Api>()
Объект сам достаёт зависимости из глобального реестра. Минусы: из сигнатуры не видно, что нужно классу, а отсутствие регистрации обнаруживается крэшем во время выполнения. Если контейнер DI доступен из любого места, он превращается в локатор.
Скоупы
Время жизни зависимостей следует за временем жизни данных:
| Скоуп | Примеры |
|---|---|
| Приложение | сетевой клиент, конфигурация, аналитика |
| Сессия пользователя | профиль, корзина, кеш заказов, токены |
| Поток | состояние оформления заказа |
| Экран | view model |
Классическая ошибка: пользовательский репозиторий с кешем зарегистрирован синглтоном. После выхода и входа под другим аккаунтом мелькают чужие данные. Скоуп сессии уничтожается при выходе целиком.
Инструменты
Android:
- Hilt (поверх Dagger): граф строится кодогенерацией при компиляции, ошибки обнаруживаются при сборке. Компоненты:
SingletonComponent,ActivityRetainedComponent,ViewModelComponent,ActivityComponentи другие. - Koin: DSL на Kotlin, граф собирается при запуске, проще вход, ошибки ловят тестом проверки графа. Работает в Kotlin Multiplatform.
iOS: стандартного фреймворка нет. Чаще ручной DI через инициализаторы и фабрики, иногда контейнеры (Swinject, Factory, Needle с проверкой при компиляции). В SwiftUI сервисы удобно передавать через Environment.
Flutter: Provider и Riverpod (зависимости через дерево и провайдеры с проверкой типов), get_it (сервис-локатор), injectable для генерации регистрации.
React Native: чаще контекст React и модули, реже контейнеры.
Как выбирать
- Большой проект и много команд: проверка графа при компиляции (Hilt, Needle).
- Небольшой проект: Koin, ручной DI, Riverpod.
- В любом случае: зависимости через конструктор, интерфейсы на границах, контейнер только в composition root.
Частые ошибки
- Вызов контейнера из любых классов.
- Синглтоны для пользовательских данных.
- Зависимость от конкретных классов вместо интерфейсов там, где нужна подмена.
- Огромный модуль DI, который знает обо всём приложении.
Что спросят на собесе
- Что такое DI и зачем он нужен?
- Чем DI отличается от Service Locator?
- Какие скоупы бывают и как сделать скоуп сессии?
- Hilt или Koin, ручной DI или контейнер на iOS?
- Как DI помогает тестам?