Koin — co to jest, zasady Dependency Injection i jak działa

Autor: IT Sectr Opublikowano: 2026-05-04 Czas czytania: 8 min

Koin to framework DI dla Kotlin, działający bez generowania kodu, refleksji i adnotacji. Biblioteka używa DSL do opisywania modułów i wstrzykuje zależności przez lekki kontener z obsługą Android, Ktor i Multiplatform. Według oficjalnej dokumentacji Koin, framework dostarcza moduły, scope’y i wbudowaną obsługę Jetpack Compose z minimalnym boilerplate.

Najważniejsze

  • Koin — framework DI dla Kotlin, używający DSL bez refleksji i generowania kodu.
  • Moduł — logiczne grupowanie rejestracji zależności przez funkcje single i factory.
  • single — rejestracja singletonu, którego instancja tworzona jest jeden raz.
  • factory — rejestracja, przy której każde żądanie tworzy nową instancję.
  • Scope — mechanizm wiązania czasu życia zależności z komponentem, na przykład z Activity.

Czym jest Koin i czym się różni

Koin — framework DI dla Kotlin, napisany w czystym języku bez użycia refleksji, adnotacji ani generowania kodu. W przeciwieństwie do Dagger Hilt, który wymaga procesora adnotacji i generowania kodu na etapie kompilacji, Koin działa wyłącznie w runtime przy użyciu lekkiego DSL do opisywania modułów.

Główna idea Koin to dostarczenie prostego API do rejestracji i rozwiązywania zależności bez konieczności uczenia się skomplikowanych koncepcji grafów zależności i drzew komponentów. Deweloper opisuje, które klasy są dostępne dla kontenera, a Koin automatycznie wstrzykuje je przez konstruktor lub leniwe delegaty by inject. Framework jest w pełni kompatybilny z Kotlin Multiplatform, co pozwala na użycie jednolitego podejścia DI na Android, iOS i stronie serwerowej.

Według ankiety Kotlin Developers Community (2025), Koin jest używany w 31% komercyjnych projektów Android, ustępując popularności tylko Hilt (47%). Główny powód wyboru — prostota konfiguracji i brak konieczności generowania kodu, co przyspiesza budowę projektu.

Wybierz Koin dla średnich i dużych projektów, gdzie ważny jest szybki start rozwoju, lub dla rozwiązań Kotlin Multiplatform, gdzie Hilt jest niedostępny ze względów architektonicznych.

Koin nie używa refleksji ani generowania kodu — wszystkie rejestracje opierają się na funkcjach inline z typami reified, które na etapie kompilacji podstawiają konkretny typ w ciele funkcji. To czyni Koin jednym z najlżejszych frameworków DI pod względem rozmiaru końcowego APK: dodanie Koin zwiększa rozmiar aplikacji tylko o 100–150 KB, podczas gdy Dagger Hilt dodaje około 500 KB kosztem wygenerowanego kodu.

Jak działa kontener i DSL Koin

Kontener Koin jest inicjowany przez funkcję startKoin, która przyjmuje lambdę z konfiguracją. Wewnątrz tej lambdy opisywane są moduły z rejestracjami — podstawowy budulec logiki DI.

startKoin i moduły

Funkcja startKoin tworzy globalny kontener dostępny z dowolnego miejsca aplikacji przez GlobalContext, jednak w projektach wielomodułowych zaleca się używanie KoinApplication do tworzenia izolowanych kontenerów. W Android do inicjalizacji używa się AndroidContext, który automatycznie wiąże się z cyklem życia Application. Moduły są rejestrowane przez parametr modules, przyjmujący listę instancji Module.

kotlin
val networkModule = module {
    single {
        OkHttpClient()
    }
    single {
        Retrofit.Builder()
            .baseUrl("https://api.example.com")
            .build()
    }
}

startKoin {
    modules(networkModule)
}

Każdy moduł zawiera definicje przez single (singleton) lub factory (nowa instancja). Definicje mogą odwoływać się do innych zarejestrowanych zależności przez get(), co tworzy graf wstrzykiwania bez jawnego określania typów i bez boilerplate.

DSL i funkcje inline

Koin aktywnie używa funkcji inline z parametrami reified do wnioskowania typu z kontekstu. Pozwala to pisać rejestracje bez jawnego określania klasy: single { MyService() } automatycznie określa typ na podstawie wartości zwracanej przez lambdę.

W przeciwieństwie do Dagger, Koin nie sprawdza grafu zależności na etapie kompilacji — wszystkie błędy są wykrywane w runtime przy pierwszym dostępie do nierozwiązanej zależności. To kompromis, który znacznie upraszcza kod i przyspiesza budowę, ale wymaga pokrycia testowego konfiguracji DI. Wiele zespołów wybiera Koin właśnie za szybkość rozwoju i prostotę, mimo braku kontroli w czasie kompilacji.

W wersji Koin 3.5 pojawiło się eksperymentalne sprawdzanie grafu na etapie kompilacji przez plugin Koin Annotations. Deweloper dodaje adnotacje @Module i @KoinComponent, a plugin generuje kod walidacji, który uruchamia się podczas budowy. Jednak główna zaleta Koin — brak generowania kodu — w tym trybie zostaje utracona, dlatego większość zespołów nadal używa klasycznego podejścia DSL z kontrolami w runtime poprzez testy.

Sposoby wstrzykiwania zależności w Koin

Koin oferuje kilka sposobów wstrzykiwania zależności: by inject(), get() i bezpośrednie przekazanie przez konstruktor. Wybór sposobu zależy od kontekstu użycia.

by inject() — leniwe wstrzykiwanie

Delegat by inject — najczęstszy sposób wstrzykiwania w ViewModel i fragmentach Android. Zależność jest inicjowana leniwie — dopiero przy pierwszym dostępie do właściwości. Jest to efektywne dla zasobożernych serwisów, które mogą nie być potrzebne od razu.

kotlin
class MainViewModel : ViewModel() {
    private val repository: UserRepository by inject()

    fun loadUsers() {
        repository.fetchAll()
    }
}

get() — jawne pobieranie

Funkcja get zwraca instancję zależności natychmiast. Używana wewnątrz lambd fabrycznych podczas rejestracji lub gdy zależność jest potrzebna w kontekście synchronicznym bez leniwej inicjalizacji. W przeciwieństwie do by inject(), get() nie obsługuje leniwego ładowania i wymaga, aby kontener był już zainicjowany w momencie wywołania.

Moduły i scope’y w Koin

Scope w Koin — mechanizm wiązania czasu życia zależności z konkretnym komponentem, takim jak Activity, Fragment czy sesja użytkownika. To kluczowa funkcjonalność do zarządzania pamięcią w aplikacjach Android.

scope — wiązanie z komponentem

Funkcja scope wewnątrz modułu tworzy scope, który żyje tak długo, jak żyje powiązany komponent. Wszystkie zależności zarejestrowane w scopie są niszczone po jego zamknięciu, co zapobiega wyciekom pamięci.

kotlin
val userScope = module {
    scope<UserSession> {
        scoped {
            UserRepository(get())
        }
        scoped {
            SessionManager(get())
        }
    }
}

Funkcja scoped rejestruje zależność, która będzie istnieć tylko wewnątrz scope’u. Po zamknięciu scope’u wszystkie obiekty scoped stają się niedostępne dla garbage collectora.

single vs factory

single — rejestracja pojedynczej instancji na całą aplikację z leniwą inicjalizacją. Używane dla serwisów bez stanu: klientów sieciowych, cache’y, loggerów.

factory — każde wywołanie get() tworzy nową instancję. Stosowane dla ViewModel, repozytoriów i obiektów ze stanem, gdzie ważna jest świeża instancja przy każdym dostępie.

Koin w projektach Android

Integracja Koin w projekcie Android jest minimalna: wystarczy dodać zależność w build.gradle i wywołać startKoin w Application.onCreate. Koin dostarcza moduły do integracji z Jetpack Compose, Navigation i WorkManager, co czyni go pełnoprawną alternatywą dla Hilt.

Specjalna biblioteka koin-android-compose umożliwia wstrzykiwanie zależności bezpośrednio w funkcjach Composable przez funkcje koinViewModel() i koinInject(). To eliminuje konieczność przekazywania kontenera przez parametry każdego ekranu i sprawia, że kod ViewModel jest czystszy dzięki automatycznemu powiązaniu z cyklem życia.

Według Google I/O 2024, Jetpack Compose stał się głównym frameworkiem dla nowych projektów Android. Koin zapewnia natywną obsługę Compose bez dodatkowej konfiguracji, automatycznie wiążąc scope’y z cyklem życia ViewModel przez koinViewModel() z uwzględnieniem kontekstu coroutine.

Do testowania Koin dostarcza funkcje koinTest i koinTestRule, tworzące izolowany kontener testowy z modułami testowymi i automatycznie zamykające go po zakończeniu testu. Zapewnia to izolację testów i zapobiega wyciekom stanów między przypadkami testowymi.

Integracja Koin z Jetpack Navigation jest realizowana przez moduł koin-androidx-navigation. ViewModel każdego ekranu automatycznie otrzymuje zależności przez by viewModel() z przekazaniem SavedStateHandle do zachowania stanu przy obrocie ekranu i przywrócenia po zminimalizowaniu aplikacji.

Do testów jednostkowych ViewModel z Koin używa się koinTestRule z biblioteki koin-test-junit5 lub koin-test-junit4. Reguła tworzy izolowany kontener z modułami testowymi przed każdym testem i automatycznie go zamyka po zakończeniu, zapobiegając wyciekom stanów między przypadkami testowymi. Rzeczywiste zależności są zastępowane mockami przez MockK: moduł z rejestracjami single { mockk() } nadpisuje główny moduł, a ViewModel w teście otrzymuje przewidywalne zachowanie zależności.

Jedną z kluczowych możliwości Koin 3.x jest obsługa Ktor do tworzenia aplikacji serwerowych w Kotlin oraz Compose Multiplatform do aplikacji desktopowych. To czyni Koin jedynym frameworkiem DI pokrywającym wszystkie trzy platformy Kotlin bez zmiany paradygmatu wstrzykiwania. Moduł koin-ktor umożliwia rejestrowanie zależności przez install(Koin) w bloku Application i wstrzykiwanie serwisów w routed przez by inject() tak samo jak w Android. To czyni Koin uniwersalnym rozwiązaniem DI dla projektów Kotlin dowolnej architektury — od klienta mobilnego po backend serwerowy.

Integracja Koin z Jetpack Navigation przez moduł koin-androidx-navigation eliminuje konieczność ręcznego tworzenia ViewModelProvider.Factory dla każdego ekranu. Dla projektów wielomodułowych Koin obsługuje leniwe ładowanie modułów przez loadKoinModules, co pozwala podłączać konfigurację DI każdego modułu feature niezależnie.

Często zadawane pytania

Czym różni się Koin od Dagger Hilt?

Koin działa w runtime bez generowania kodu i adnotacji, co przyspiesza budowę, ale nie sprawdza grafu zależności na etapie kompilacji. Hilt generuje kod podczas kompilacji i łapie błędy DI wcześniej, ale wymaga złożonej konfiguracji i spowalnia budowę.

Czy Koin obsługuje Kotlin Multiplatform?

Tak, Koin w pełni obsługuje Kotlin Multiplatform. Biblioteka koin-core działa na wszystkich platformach Kotlin, a koin-android i koin-compose dodają możliwości specyficzne dla platform Android i iOS.

Jak obsługiwać zależności cykliczne w Koin?

Zależności cykliczne prowadzą do StackOverflowError w runtime. Koin nie wykrywa ich automatycznie. Rozwiązanie — refaktoring architektury: wydzielenie wspólnego interfejsu, użycie wzorca Listener/Observer lub przerwanie cyklu przez fabrykę z późną inicjalizacją.

Czy trzeba zamykać scope’y ręcznie?

W Android scope’y można powiązać z cyklem życia Activity lub Fragment przez AndroidScope. Przy zniszczeniu komponentu Koin automatycznie zamyka odpowiedni scope. W scope’ach niestandardowych (sesja użytkownika) zamknięcie wykonuje się ręcznie przez wywołanie scope.close.

Jak testować kod z Koin?

Użyj funkcji koinTest z modułu koin-test. Tworzy ona izolowany kontener z modułami testowymi, który automatycznie zamyka się po teście. Rzeczywiste zależności są zastępowane mockami przez moduł z Mockito lub MockK.

Podsumowanie

  • Koin — framework DI dla Kotlin z DSL bez refleksji, adnotacji i generowania kodu.
  • Moduły grupują rejestracje przez single (singleton) i factory (nowa instancja).
  • startKoin inicjuje globalny kontener dostępny przez by inject() i get().
  • Scope wiąże czas życia zależności z cyklem życia komponentu Android.
  • Wstrzykiwanie w Compose odbywa się przez koinViewModel() i koinInject().
  • Koin obsługuje Kotlin Multiplatform, umożliwiając jednolite podejście DI na wszystkich platformach.
  • Błędy DI są wykrywane w runtime, dlatego pokrycie testowe konfiguracji jest obowiązkowe.

Opracujemy aplikację mobilną pod klucz

IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.

Omów projekt

Przeczytaj również