| name | koin-editor |
| description | Configures and uses Koin for Dependency Injection in Android and KMP projects. Use when user asks to "set up Koin", "add dependency injection", "register a new dependency", "create a Koin module", or troubleshoot injection failures. |
Koin Editor
Overview
Koin is a lightweight Kotlin DI framework that uses a DSL for declaring dependencies. No code generation, no reflection — just Kotlin.
Setup
Dependencies
[versions]
koin = "3.5.6"
[libraries]
koin-android = { module = "io.insert-koin:koin-android", version.ref = "koin" }
koin-androidx-compose = { module = "io.insert-koin:koin-androidx-compose", version.ref = "koin" }
koin-test = { module = "io.insert-koin:koin-test", version.ref = "koin" }
koin-test-junit4 = { module = "io.insert-koin:koin-test-junit4", version.ref = "koin" }
dependencies {
implementation(libs.koin.android)
implementation(libs.koin.androidx.compose)
testImplementation(libs.koin.test)
testImplementation(libs.koin.test.junit4)
}
Initialization
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidLogger(Level.DEBUG)
androidContext(this@MyApplication)
modules(
appModule,
repositoryModule,
domainModule,
viewModelModule,
)
}
}
}
Module Definition
Basic Module
val repositoryModule = module {
single<UserRepository> {
UserRepositoryImpl(
localDataSource = get(),
remoteDataSource = get(),
)
}
factory<UserLocalDataSource> {
UserLocalDataSourceImpl(database = get())
}
viewModel {
ProfileViewModel(
getUserUseCase = get(),
updateUserUseCase = get(),
)
}
}
Scopes (for Session-Scoped Dependencies)
val sessionModule = module {
scope<UserSession> {
scoped { SessionCart() }
}
}
val scope = getKoin().createScope("session-1", named<UserSession>())
val cart: SessionCart = scope.get()
scope.close()
Scope Reference
| Function | Creates | Lifetime |
|---|
single { } | One instance | App lifetime |
factory { } | New instance per get() | Per injection |
viewModel { } | ViewModel | ViewModel lifecycle |
scoped { } | One per scope | Scope lifetime |
Qualifiers (Named Dependencies)
val appModule = module {
single<CoroutineDispatcher>(named("IO")) { Dispatchers.IO }
single<CoroutineDispatcher>(named("Default")) { Dispatchers.Default }
single<CoroutineDispatcher>(named("Main")) { Dispatchers.Main }
}
factory {
MyRepository(
ioDispatcher = get(named("IO")),
)
}
class MyRepository(
@Named("IO") private val ioDispatcher: CoroutineDispatcher,
)
Injecting Dependencies
Constructor Injection (preferred)
class GetOrdersUseCase(
private val orderRepository: OrderRepository,
private val ioDispatcher: CoroutineDispatcher,
)
factory {
GetOrdersUseCase(
orderRepository = get(),
ioDispatcher = get(named("IO")),
)
}
In Activities / Fragments
class HomeActivity : AppCompatActivity() {
private val viewModel: HomeViewModel by viewModel()
private val analytics: AnalyticsTracker by inject()
}
In Compose
@Composable
fun HomeScreen() {
val viewModel: HomeViewModel = koinViewModel()
}
Lazy Injection
class HomeFragment : Fragment() {
private val tracker: AnalyticsTracker by inject()
}
Module Organization (Best Practice)
Centralize all module files in your app module:
app/src/main/java/com/example/di/
├── AppModule.kt # Context, dispatchers, framework dependencies
├── RepositoryModule.kt # Repositories and data sources
├── DomainModule.kt # Use Cases
└── ViewModelModule.kt # ViewModels
val appModule = module {
single { androidContext().getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager }
single<CoroutineDispatcher>(named("IO")) { Dispatchers.IO }
single<CoroutineDispatcher>(named("Default")) { Dispatchers.Default }
}
val repositoryModule = module {
single<OrderRepository> {
OrderRepositoryImpl(
localDataSource = get(),
remoteDataSource = get(),
ioDispatcher = get(named("IO")),
)
}
single<OrderLocalDataSource> { OrderRoomDataSource(database = get()) }
single<OrderRemoteDataSource> { OrderApiDataSource(api = get()) }
}
val domainModule = module {
factory { GetOrdersUseCase(repository = get(), ioDispatcher = get(named("IO"))) }
factory { PlaceOrderUseCase(repository = get(), ioDispatcher = get(named("IO"))) }
}
val viewModelModule = module {
viewModel { OrderListViewModel(getOrders = get()) }
viewModel { params -> OrderDetailViewModel(getOrder = get(), orderId = params.get()) }
}
Testing with Koin
Option 1: KoinTest (JUnit4)
@RunWith(AndroidJUnit4::class)
class OrderRepositoryTest : KoinTest {
private val repository: OrderRepository by inject()
@get:Rule
val koinTestRule = KoinTestRule.create {
modules(
module {
single<OrderRepository> { OrderRepositoryImpl(get(), get(), get(named("IO"))) }
single<OrderLocalDataSource> { mockk() }
single<OrderRemoteDataSource> { mockk() }
single<CoroutineDispatcher>(named("IO")) { UnconfinedTestDispatcher() }
}
)
}
@Test
fun `SHOULD return orders`() = runTest {
val result = repository.getOrders()
assertTrue(result.isSuccess)
}
}
Option 2: Override Module in Test (cleaner for unit tests)
For unit testing ViewModels and Use Cases, prefer plain constructor injection over Koin's test utilities:
class OrderViewModelTest {
private val mockRepo: OrderRepository = mockk()
private val sut = OrderListViewModel(getOrders = GetOrdersUseCase(mockRepo, UnconfinedTestDispatcher()))
}
Troubleshooting
| Error | Cause | Fix |
|---|
NoBeanDefFoundException | Dependency not declared in any module | Add single/factory for the type |
InstanceCreationException | Constructor threw an exception | Check constructor params / get() calls |
| Circular dependency | A depends on B, B depends on A | Introduce a third type or use lazy { get() } |
| Wrong dispatcher injected | Missing named() qualifier | Add named("IO") to declaration and injection |
Debug Mode
startKoin {
androidLogger(Level.DEBUG)
}
Checklist: New Dependency
References