Введение

«У вас в проекте Koin? А почему не Hilt?» — вопрос, после которого половина кандидатов начинает мямлить. И зря: Koin — не «игрушечный DI для маленьких проектов», а полноценный фреймворк со своими сильными сторонами. Разберём, как он устроен, и научимся защищать этот выбор на собеседовании.

Koin — это DI-контейнер для Kotlin, который работает без кодогенерации: зависимости описываются декларативно на самом Kotlin, а граф собирается в рантайме. Никаких аннотаций-обработчиков, никакого KSP — просто подключил библиотеку и написал модуль.

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

Что такое Koin: DI без кодогенерации

С точки зрения архитектуры Koin — это Service Locator с DSL поверх него: контейнер хранит определения объектов и по запросу возвращает экземпляры, самостоятельно разрешая их зависимости. В этом его принципиальное отличие от Dagger и Hilt, которые генерируют код Dagger-компонентов при компиляции.

Чем это оборачивается на практике:

  • Простота подключения. Одна зависимость в Gradle, никаких плагинов и процессоров аннотаций.
  • Читаемые модули. Описание графа — это обычный Kotlin-код: его легко читать, рефакторить и переиспользовать между модулями.
  • Быстрый старт. Не нужно изучать компоненты, скоупы и иерархии — три ключевых слова DSL закрывают 90% задач.
  • Цена — безопасность типов. Неверная привязка выстрелит в рантайме (часто при первом обращении к зависимости), а не на сборке.

Как отвечать на вопрос «зачем Koin»

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

Модули и DSL: single и factory

Вся конфигурация Koin живёт в модулях. Модуль — это просто функция module { ... }, внутри которой объявляются зависимости:

AppModule.kt
val appModule = module {
    single<NotesRepository> { NotesRepositoryImpl() }
    single<ApiService> { RetrofitProvider.create() }
    factory { NotesPresenter(get()) }
}

Два ключевых слова, которые обязательно спрашивают на собеседовании:

  • single — создаёт один экземпляр на всё приложение. Подходит для репозиториев, API-клиентов, баз данных — всего, что должно жить в единственном числе.
  • factory — создаёт новый экземпляр при каждом запросе. Подходит для объектов с коротким временем жизни: например, презентеров, которые не должны держаться в памяти между экранами.

Внутри конструкций виден ещё один важный механизм — get(): так Koin понимает, что зависимость нужно взять из контейнера. NotesPresenter(get()) означает «создай NotesPresenter, передав ему найденный в графе экземпляр NotesRepository».

Если класс можно создать через конструктор автоматически, используется сокращённая форма с bind для привязки интерфейса к реализации:

AppModule.kt
val appModule = module {
    single<NotesRepositoryImpl>() bind NotesRepository::class
}

Скоупы тоже есть: scope { ... } позволяет ограничить время жизни группы объектов, например, в рамках сессии пользователя. Но в большинстве проектов достаточно single и factory.

Типичная ошибка новичка — делать всё single, потому что «так меньше кода». В итоге фабрики, которые должны создаваться на каждый экран, превращаются в долгоживущие синглтоны, и в приложении появляются утечки и общий мутируемый стейт. Вопрос «почему здесь single, а не factory» — один из моих любимых на собеседованиях.

Koin в Android: старт, инжекция, ViewModel

Для Android Koin подключается одной зависимостью — io.insert-koin:koin-android. Запуск происходит в классе Application:

App.kt
class App : Application() {
    override fun onCreate() {
        super.onCreate()
        startKoin {
            androidLogger()
            androidContext(this@App)
            modules(appModule)
        }
    }
}
  • androidLogger() — логирование работы контейнера;
  • androidContext() — передаёт контекст приложения, чтобы Koin мог отдавать его как зависимость;
  • modules() — список модулей, из которых собирается граф.

Инжекция в Android-компоненты — через делегат by inject():

MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val notesRepository: NotesRepository by inject()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        // notesRepository уже готов к использованию
    }
}

А для ViewModel в модуле объявляется специальное слово viewModel, а в Activity — делегат by viewModel():

NotesViewModel.kt
class NotesViewModel(
    private val repository: NotesRepository
) : ViewModel() {

    val notes: StateFlow<List<Note>> = repository.notes
}
AppModule.kt
val appModule = module {
    single<NotesRepository> { NotesRepositoryImpl() }
    viewModel { NotesViewModel(get()) }
}
MainActivity.kt
class MainActivity : AppCompatActivity() {

    private val viewModel: NotesViewModel by viewModel()

    // ...
}

by viewModel() создаёт нужную ViewModelProvider.Factory автоматически и привязывает ViewModel к жизненному циклу — вручную фабрику писать не нужно. Это, кстати, ещё один плюс Koin перед ручным DI: ViewModelFactory — одно из самых скучных мест в Android-коде.

  • Подключение: зависимость koin-android, запуск через startKoin в Application.
  • Модули: module { ... } — место объявления зависимостей; single — синглтон, factory — новый экземпляр на запрос.
  • Инжекция: by inject() для обычных объектов, by viewModel() для ViewModel.
  • Связывание зависимостей: get() внутри конструкций и bind для привязки интерфейса к реализации.
  • Цена простоты: граф не проверяется при компиляции, ошибки приходят из рантайма.

Koin vs Hilt: что выбрать

На собеседовании вопрос «Koin или Hilt» — это проверка не знания библиотек, а умения взвешивать инженерные компромиссы. Хороший ответ начинается не с «я люблю Koin», а с контекста.

КритерийKoinHilt
КодогенерацияНетДа (Dagger-код при компиляции)
Ошибки графаВ рантаймеПри сборке
НастройкаОдна зависимостьПлагин + обработчик аннотаций
Порог входаНизкийСредний
Скорость разрешенияМедленнееБыстрее
Интеграция с AndroidХорошаяОфициальный стандарт Google

Когда выбирать Koin:

  • проект на Kotlin Multiplatform — Koin работает на всех платформах;
  • небольшая кодовая база, где граф простой и «читается глазами»;
  • команда хочет минимум магии и понятный каждому код;
  • быстрое прототипирование без лишней настройки.

Когда выбирать Hilt:

  • крупный проект со сложным графом — проверка при компиляции экономит часы отладки;
  • активное использование скоупов и вложенных компонентов;
  • стандартная экосистема Google, интеграции с WorkManager и Compose «из коробки».

Честный вывод

Ни Koin, ни Hilt не «лучше» в вакууме. На собеседовании засчитывается ответ: «Мы выбрали Koin, потому что проект небольшой и мы ценим читаемость DSL; если граф разрастётся, миграция на Hilt будет стоить пару недель — и это осознанный риск». Умение называть цену решения — признак middle-мышления.

Пара слов обо мне: я Рустем Бикбулатов, senior Android-разработчик и ментор Яндекс Практикума. Провожу технические собеседования, за плечами более 100 разборов кандидатов. Помогаю разработчикам с опытом 1–2 года закрыть пробелы в Kotlin, Coroutines и архитектуре и выйти на middle за 4–8 недель.

Нужна помощь в переходе на middle?

Бесплатная диагностика: разберу ваши пробелы в DI и архитектуре и составлю план роста за 30–40 минут. Без обязательств — если менторство вам не подойдёт, честно скажу.

Итоги

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

  • Суть: DSL-модули на Kotlin, граф собирается в рантайме, без кодогенерации и аннотаций.
  • Ключевые слова: single — синглтон, factory — новый экземпляр, viewModel — для ViewModel, by inject() и by viewModel() — для инжекции.
  • Компромисс: простота и скорость подключения в обмен на проверку графа только в рантайме.

Главное, что нужно запомнить

На собеседовании не спрашивают «что такое Koin» — спрашивают «почему Koin, а не Hilt». Готовый ответ: Koin — это DI без кодогенерации, где зависимости описываются Kotlin-модулями и резолвятся в рантайме; он проще, но Hilt даёт проверку графа при компиляции и официальный статус стандарта.

Если не уверены, что сможете так же уверенно ответить про остальные темы собеседования — архитектуру, корутины, жизненный цикл — не нужно зубрить всё подряд. Напишите в Telegram, разберём вашу ситуацию и составим план, что закрывать в первую очередь.