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, 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.
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.
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.
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.
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.
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.
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.
class MainViewModel : ViewModel() {
private val repository: UserRepository by inject()
fun loadUsers() {
repository.fetchAll()
}
}
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.
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.
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.
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 — 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.
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
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
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ę.
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.
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ą.
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.
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
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.
Przeczytaj również