Урок по треку Android 7 мин

DI: Hilt и Koin за 7 минут

Внедрение зависимостей, компоненты и скоупы Hilt, модули, Koin и чем они отличаются.

Внедрение зависимостей (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?
  • Как подменить зависимость в тесте?

Закрепите за 5 минут

После урока в тренажёре 3-5 проверочных вопросов, дальше модуль уходит в повторения.