Irbisa · Шпаргалка

Dependency Injection в Android

Hilt, Dagger, Koin, scope-ы, модули, тестируемость

Middle-разработчик · 24 вопроса

  1. 01

    Что такое внедрение зависимостей и как оно связано с Dependency Inversion, IoC и service locator?

    Внедрение зависимостей (DI) означает, что класс получает свои зависимости снаружи, обычно через конструктор, а не создаёт их сам и не достаёт из глобального объекта.

    class UserRepository(private val api: UserApi, private val dao: UserDao)class UserViewModel(private val repo: UserRepository) : ViewModel() // Composition root: the one place that knows how to build everythingclass AppContainer(context: Context) {  private val retrofit = Retrofit.Builder().baseUrl("https://api.example.com/").build()…
  2. 02

    Внедрение через конструктор или через поля: что вы предпочитаете и почему Hilt в Activity и Fragment использует поля?

    Внедрение через конструктор служит вариантом по умолчанию: объект просто не может существовать без своих зависимостей.

    @AndroidEntryPointclass MainActivity : AppCompatActivity() {   @Inject lateinit var analytics: AnalyticsTracker   // field injection, not private  private val vm: MainViewModel by viewModels()      // built by Hilt's factory…
  3. 03

    Что делают @Inject, @Module, @Provides и @Component в Dagger и когда на самом деле строится граф?

    @Inject помечает конструкторы, которые Dagger может вызывать, @Module с @Provides собирает типы, которые сам он создать не может, а @Component помечает интерфейс, который Dagger реализует, чтобы связать одно с другим; граф проверяется при компиляции, а экземпляры создаются лениво во время выполнения.

    class UserRepository @Inject constructor(private val api: UserApi) @Moduleobject NetworkModule {  @Provides @Singleton  fun retrofit(): Retrofit =…
  4. 04

    Что такое Hilt и что он добавляет поверх Dagger 2?

    Hilt представляет собой DI-библиотеку Google для Android: Dagger 2 с заранее заданным деревом компонентов, привязанным к жизненным циклам Android, поэтому вы ставите аннотации на классы, а не пишете и связываете компоненты сами.

    @HiltAndroidAppclass MyApp : Application() @HiltViewModelclass HomeViewModel @Inject constructor(  private val repo: UserRepository,…
  5. 05

    Когда использовать @Binds вместо @Provides и можно ли держать их в одном модуле?

    @Binds говорит Dagger, что один тип удовлетворяется уже существующим биндингом другого типа, поэтому генерируется только псевдоним; @Provides выполняет тело функции, которое написали вы.

    interface AnalyticsTracker { fun log(event: String) } class FirebaseTracker @Inject constructor(  private val firebase: FirebaseAnalytics) : AnalyticsTracker {  override fun log(event: String) = firebase.logEvent(event, null)…
  6. 06

    Как скоупы Hilt соответствуют времени жизни компонентов и почему не стоит скоупить всё подряд?

    Скоуп делает биндинг одним экземпляром на экземпляр своего компонента, так что выбор скоупа равносилен утверждению о том, сколько живёт объект; биндинги без скоупа создаются заново в каждой точке внедрения, и это правильное поведение по умолчанию.

  7. 07

    Memory Profiler показывает три живых экземпляра OkHttpClient, хотя этот модуль предоставляет всего один. Что пошло не так?

    @Module@InstallIn(SingletonComponent::class)object NetworkModule {  @Provides  fun okHttp(auth: AuthInterceptor): OkHttpClient =    OkHttpClient.Builder().addInterceptor(auth).build()} class FeedApiClient @Inject constructor(private val client: OkHttpClient)class ImageLoader @Inject constructor(private val client: OkHttpClient)class UploadService @Inject constructor(private val client: OkHttpClient)

    У метода @Provides нет скоупа, значит, это рецепт, а не экземпляр: Dagger выполняет его заново для каждой точки внедрения, и три потребителя получают три клиента.

  8. 08

    Найдите баг: экран товара добавляет позицию в корзину, а бейдж корзины в тулбаре остаётся на 0.

    @ViewModelScopedclass CartState @Inject constructor() {  private val _items = MutableStateFlow<List<Item>>(emptyList())  val items: StateFlow<List<Item>> = _items.asStateFlow()  fun add(item: Item) = _items.update { it + item }} @HiltViewModelclass ProductViewModel @Inject constructor(private val cart: CartState) : ViewModel() {  fun addToCart(item: Item) = cart.add(item)} @HiltViewModelclass CartBadgeViewModel @Inject constructor(cart: CartState) : ViewModel() {  val count: Flow<Int> = cart.items.map { it.size }}

    @ViewModelScoped значит «один экземпляр на ViewModel», а не «один на все ViewModel»: каждый @HiltViewModel получает свой ViewModelComponent, поэтому ProductViewModel и CartBadgeViewModel держат два разных объекта CartState, и бейдж следит за пустым.

  9. 09

    Эта @HiltViewModel запрашивает @ActivityContext. Что произойдёт при сборке и какой Context ей нужен?

    @HiltViewModelclass ProfileViewModel @Inject constructor(  private val repo: ProfileRepository,  @ActivityContext private val context: Context) : ViewModel() {  private val prefs =    context.getSharedPreferences("profile", Context.MODE_PRIVATE)}

    Сборка падает с [Dagger/MissingBinding]: ViewModelComponent находится под ActivityRetainedComponent, который переживает любую отдельную Activity, поэтому @ActivityContext ему взять неоткуда.

  10. 10

    Зачем нужны @Qualifier и мультибиндинги (@IntoSet, @IntoMap), с практическим примером?

    @Qualifier говорит Dagger, какой из нескольких биндингов одного типа вы имеете в виду, а мультибиндинг собирает много биндингов в один Set или Map.

    @Qualifier @Retention(AnnotationRetention.BINARY)annotation class AuthClient @Module@InstallIn(SingletonComponent::class)object NetworkModule {…
  11. 11

    Тест репозитория сжигает реальные секунды и падает по таймауту на медленном CI, потому что репозиторий жёстко использует Dispatchers.IO. Как перестроить его через DI?

    Захардкоженный диспетчер оказывается замаскированной зависимостью: внедрите CoroutineDispatcher с квалификатором, чтобы тест мог передать TestDispatcher и управлять временем.

    @Qualifier @Retention(AnnotationRetention.BINARY) annotation class IoDispatcher@Qualifier @Retention(AnnotationRetention.BINARY) annotation class ApplicationScope @Module@InstallIn(SingletonComponent::class)object CoroutinesModule {…
  12. 12

    ViewModel экрана деталей нужен orderId, с которым экран открыли. Как значение времени выполнения попадает во внедряемый класс?

    Значение, известное только в рантайме, не может быть биндингом, поэтому оно приходит через SavedStateHandle или через assisted-фабрику.

    class OrderLoader @AssistedInject constructor(  @Assisted private val orderId: String,   // runtime value  private val repo: OrderRepository        // from the graph) {  suspend fun load(): Order = repo.load(orderId)…
  13. 13

    Что дают Provider<T> и Lazy<T> по сравнению с прямым внедрением T?

    Оба откладывают создание до вызова get(); Provider разрешает биндинг при каждом вызове, а Lazy создаёт объект один раз при первом вызове и кэширует результат.

    class ReportGenerator @Inject constructor(  private val db: Lazy<AppDatabase>,          // dagger.Lazy: opened on first get(), then cached  private val sessions: Provider<Session>     // a fresh Session on every get()) {  fun run(): Report {    val dao = db.get().reports()              // the database opens only if run() is called…
  14. 14

    Найдите баг: после обновления access-токена все запросы продолжают уходить со старым токеном, пока приложение не перезапустят.

    @Module@InstallIn(SingletonComponent::class)object NetworkModule {  @Provides  fun authToken(store: TokenStore): AuthToken = store.current()   @Provides @Singleton  fun client(auth: AuthInterceptor): OkHttpClient =    OkHttpClient.Builder().addInterceptor(auth).build()} class AuthInterceptor @Inject constructor(private val token: AuthToken) : Interceptor {  override fun intercept(chain: Interceptor.Chain): Response = chain.proceed(    chain.request().newBuilder().header("Authorization", "Bearer ${token.value}").build()  )}

    AuthInterceptor читает AuthToken один раз, в конструкторе, а @Singleton OkHttpClient создаёт этот интерсептор тоже один раз.

  15. 15

    Этот сетевой модуль падает с [Dagger/DependencyCycle]. Где цикл и как его правильно разорвать?

    class AuthInterceptor @Inject constructor(  private val tokens: TokenRefresher) : Interceptor { /* adds the Authorization header */ } class TokenRefresher @Inject constructor(private val api: AuthApi) @Module@InstallIn(SingletonComponent::class)object NetworkModule {  @Provides @Singleton  fun client(auth: AuthInterceptor): OkHttpClient =    OkHttpClient.Builder().addInterceptor(auth).build()   @Provides @Singleton  fun authApi(client: OkHttpClient): AuthApi =    Retrofit.Builder().baseUrl("https://api.example.com/").client(client).build().create(AuthApi::class.java)}

    Цикл такой: OkHttpClient -> AuthInterceptor -> TokenRefresher -> AuthApi -> OkHttpClient; разорвать его нужно отдельным клиентом с квалификатором для AuthApi без auth-интерсептора, а не прятать петлю за Provider.

  16. 16

    Как внедрить зависимости в класс, которым Hilt не владеет, например Worker из WorkManager, ContentProvider или объект, который создаёт сторонняя библиотека?

    У Hilt есть отдельная интеграция для воркеров и запасной выход @EntryPoint для всего остального.

    @HiltWorkerclass SyncWorker @AssistedInject constructor(  @Assisted appContext: Context,  @Assisted params: WorkerParameters,  private val repo: FeedRepository) : CoroutineWorker(appContext, params) {…
  17. 17

    Сборка падает с [Dagger/MissingBinding] для PaymentGateway. Как читать эту ошибку и искать причину?

    Dagger дошёл до типа, для которого у него нет рецепта, а цепочка запросов под сообщением показывает, какой компонент его запросил и через кого.

    // Request chain, trimmed://   PaymentGateway is injected at CheckoutRepository(gateway, ...)//   CheckoutRepository is injected at CheckoutViewModel(repo, ...)//   CheckoutViewModel is requested at App_HiltComponents.ViewModelC// Bottom-up: ViewModelC asked, via CheckoutViewModel -> CheckoutRepository.…
  18. 18

    Как подменить зависимости фейками в тестах, если приложение использует Hilt?

    В unit-тестах Hilt не нужен: вызывайте конструкторы с фейками; в инструментальных и Robolectric-тестах используйте @HiltAndroidTest с @TestInstallIn, @UninstallModules или @BindValue, чтобы подменить биндинги.

    // Test source set: replaces the real module in every Hilt test@Module@TestInstallIn(  components = [SingletonComponent::class],  replaces = [NetworkModule::class])…
  19. 19

    Hilt или Koin: в чём реальная разница и когда что выбирать?

    Hilt строит и проверяет весь граф при компиляции; Koin разрешает зависимости в рантайме из реестра: это проще настроить, но отсутствующий биндинг переезжает из сборки в работающее приложение.

  20. 20

    Вы никогда не писали класс Hilt_MainActivity, но он появляется в стектрейсах. Что на самом деле генерируют Hilt и Dagger при сборке?

    Dagger пишет фабрику на каждый биндинг и реализации компонентов, а Gradle-плагин Hilt переписывает каждый класс с @AndroidEntryPoint так, чтобы он наследовал сгенерированный базовый класс Hilt_, который и выполняет внедрение.

    // You write:class AnalyticsTracker @Inject constructor(client: OkHttpClient) @AndroidEntryPointclass MainActivity : AppCompatActivity() {  @Inject lateinit var analytics: AnalyticsTracker…
  21. 21

    Сабкомпоненты или зависимости компонентов: чем они отличаются внутри и что подходит многомодульному приложению?

    Сабкомпонент генерируется внутри родителя и наследует все его биндинги; зависимость компонента представляет собой отдельно сгенерированный компонент, который видит только то, что родитель явно открывает через provision-методы.

    // Subcomponent: generated inside the parent, sees all its bindings@Subcomponent(modules = [CheckoutModule::class])interface CheckoutComponent {  fun inject(fragment: CheckoutFragment)  @Subcomponent.Factory interface Factory { fun create(): CheckoutComponent }}…
  22. 22

    После выхода и входа под другим пользователем приложение на мгновение показывает заказы предыдущего. При чём тут DI-граф?

    @Singleton переживает пользовательскую сессию: репозиторий, закэшировавший заказы пользователя A, оказывается тем же самым объектом, который получает пользователь B, потому что ничто в графе не привязано к факту входа.

    @Scope @Retention(AnnotationRetention.RUNTIME) annotation class SessionScope @SessionScope@DefineComponent(parent = SingletonComponent::class)interface SessionComponent…
  23. 23

    В Gradle-проекте из сорока модулей где Hilt на самом деле собирает граф и как это влияет на время сборки?

    Hilt собирает все @InstallIn-модули и entry points из classpath приложения и генерирует все компоненты один раз, в модуле приложения, поэтому по мере роста графа этот модуль становится узким местом сборки.

  24. 24

    Во что Dagger обходится большой Kotlin-кодовой базе и почему в 2026 году команды присматриваются к Metro и kotlin-inject?

    Издержки Dagger: время сборки, кодогенератор, ориентированный на Java, и слой Hilt только для Android; Metro (плагин компилятора Kotlin) и kotlin-inject (KSP) сохраняют проверку на этапе компиляции, но работают внутри тулчейна Kotlin и поддерживают мультиплатформу.