Skip to main content
Version: 4.2

Compile-Time Safety

The Koin Compiler Plugin checks your dependency graph while your code compiles. A missing definition, a wrong qualifier or a circular dependency fails the build, with the file and line, instead of crashing at runtime. This page covers what is checked, the exact diagnostics, and the cases the plugin cannot check. It assumes the plugin is installed (see Compiler Plugin Setup).

Compile-time safety is enabled by default (compileSafety = true, see Compiler Plugin Options).

Where the checks run​

The plugin checks at two levels.

Full graph: at each Koin entry point, the plugin assembles every module that the entry point loads, from your sources and from dependency JARs, and checks every definition against that complete graph.

Call sites: every get<T>(), inject<T>(), koinInject<T>(), koinViewModel<T>(), by viewModel() and similar call is checked: T must exist in the assembled graph. If a feature module makes the call but cannot see the full graph, the plugin leaves a hint in the compiled module. The app module reads these hints from its dependencies and checks them when it compiles.

Recognized entry points:

Entry pointExample
startKoinstartKoin<MyApp>(), startKoin { modules(appModule) }
koinApplicationkoinApplication { modules(appModule) }
koinConfigurationkoinConfiguration<MyApp>()
@KoinApplication@KoinApplication object MyApp
withConfiguration<T>()startKoin { withConfiguration<MyApp>() }
Ktor install(Koin)install(Koin) { modules(appModule) }

modules(...) accepts lists the plugin can follow: listOf(...), +, .toList(), a top-level val, or a function returning List<Module>:

val dataModules = listOf(databaseModule, networkModule)
fun featureModules(): List<Module> = listOf(loginModule, profileModule)

startKoin {
modules(dataModules + featureModules())
}

A module declared as a function (fun dataModule(): Module = module { ... }) is followed like a top-level val.

note

A Gradle module with no entry point of its own (a library, most feature modules) gets no missing-dependency diagnostics in its own compilation. A module checked alone cannot know how the app will wire it. Its definitions are checked when the app module assembles the graph. The plugin says so in the build output:

[Koin] compile-safety validation skipped — no Koin entry point in this compilation.

What is checked​

CaseResult
Non-nullable parameter, no definitionError
Nullable parameter (T?), no definitionOK, resolved with getOrNull()
Parameter with a default value, no definitionOK, uses the Kotlin default (with skipDefaultValues = true)
Parameter with a default value and a qualifier (@Named)Error, the qualifier forces resolution
@InjectedParam parameterOK, passed at runtime with parametersOf()
@Property("key") parameterOK. Warning if no @PropertyValue("key") default exists
List<T> parameterOK, getAll() returns an empty list if nothing matches
Lazy<T>, no definition for TError, the inner type is checked
@Named("x") parameter, no definition with that qualifierError, with a hint if the type exists with another qualifier
Scoped dependency declared in another scopeError
Circular dependency (A needs B, B needs A)Error
@Provided type or parameterOK, provided outside the graph
@ScopeId parameterOK, resolved from a named scope at runtime
Scope parameterOK, the current scope is passed
Android framework type (Context, SavedStateHandle, ...)OK, built-in list
get<T>() / inject<T>() with no definition for TError
get<T>() without parametersOf() when T has @InjectedParamError
parametersOf(...) with the wrong count or typesError

The rules for how each parameter type is resolved are on Definitions.

Checks per declaration style​

Every style and DSL form is checked against the same graph. They can live in one project: a DSL definition can depend on an annotated class, and the other way around.

val appModule = module {
single<Database>()
single<UserRepository>() // checked: Database exists
viewModel<UserViewModel>() // checked: UserRepository exists
}

The plugin reads the constructor, generates the resolution calls and checks every parameter. create(::function) is checked the same way, on the function parameters:

fun buildUserRepository(db: Database): UserRepository = UserRepository(db)

val appModule = module {
single { create(::buildUserRepository) } // checked: Database exists
}
warning

Since Compiler Plugin 1.2.0, singleOf, factoryOf, scopedOf and viewModelOf are checked. Code that relied on a missing dependency in these functions no longer compiles. The build was hiding a runtime crash.

Diagnostics​

Every diagnostic has a stable code, printed as [Koin][CODE]. Search for the code or the first line of the message.

Missing dependency (KOIN-D001)​

A definition parameter has no definition in the graph:

[Koin][KOIN-D001] Missing dependency: com.myapp.Database
required by: com.myapp.UserRepository (parameter 'db')
in module: com.myapp.AppModule

For a DSL definition, required by: starts with dsl:. When the type exists with another qualifier, a hint follows:

[Koin][KOIN-D001] Missing dependency: com.myapp.NetworkClient qualified with @Named("http")
required by: com.myapp.ApiService (parameter 'client')
in module: com.myapp.AppModule
Hint: Found similar binding: com.myapp.NetworkClient (no qualifier)

Fix: declare the missing definition, add the module that declares it to modules() or includes(), or fix the qualifier (see Qualifiers).

Missing definition at a call site (KOIN-D002, KOIN-D003)​

A get<T>(), inject<T>() or koinViewModel<T>() call asks for a type that no loaded module declares:

[Koin][KOIN-D002] Missing definition: com.myapp.Tracker
resolved by: get<Tracker>()
No matching definition found in any loaded module.
Check your declaration with Annotation or DSL.

KOIN-D003 is the same error for a call made in a dependency module, reported when the app module compiles. Its second line reads required by a call site in a dependency module.

Circular dependency (KOIN-D004)​

[Koin][KOIN-D004] Circular dependency detected:
com.myapp.A → com.myapp.B → com.myapp.A
Break with Lazy<T> injection or refactor to remove the cycle.

A Lazy<T>, nullable, List<T>, @InjectedParam, @Provided, @Property, @ScopeId or default-valued parameter does not count as an edge in the cycle. Changing one parameter to Lazy<T> breaks the cycle. See Troubleshooting for ways to remove it.

Injected parameters (KOIN-D005, KOIN-D006)​

The definition declares @InjectedParam parameters, and the call site does not pass them:

[Koin][KOIN-D006] com.myapp.Greeter requires 1 injected param(s) but get<Greeter>() has no parametersOf(...) at the call site.
Expected @InjectedParam slots: name: kotlin.String
Pass them via parametersOf(...) inside the trailing lambda.

KOIN-D005 reports a parametersOf(...) with the wrong number of arguments or the wrong types (Mismatched parametersOf(...) for ...). Both checks only run when the call contains a parametersOf(...) the plugin can read. See Injected Parameters.

Unsupported suspend binding (KOIN-D007)​

A definition whose type extends a suspend function type is rejected, because Koin cannot resolve it at runtime:

fun interface SetUsernameUseCase : suspend (String) -> Unit

@Factory
fun provideSetUsernameUseCase(repository: UserRepository): SetUsernameUseCase =
SetUsernameUseCase(repository::setUsername)
[Koin][KOIN-D007] Unsupported binding: com.myapp.SetUsernameUseCase extends kotlin.coroutines.SuspendFunction1
Suspend function injection is not yet supported by Koin runtime.
Remove the @Single / @Factory / @Scoped registration, or refactor the binding type to not extend a suspend function.
Tracked at https://github.com/InsertKoinIO/koin-compiler-plugin/issues/16

Fix: use a regular interface with a suspend method, or a class.

All codes​

CodeSeverityMessage starts with
KOIN-D001ErrorMissing dependency:
KOIN-D002ErrorMissing definition: (call site in this module)
KOIN-D003ErrorMissing definition: (call site in a dependency module)
KOIN-D004ErrorCircular dependency detected:
KOIN-D005ErrorMismatched parametersOf(...) for
KOIN-D006Error<Type> requires N injected param(s) but
KOIN-D007ErrorUnsupported binding:
KOIN-D008ErrorModule id collision: (two module names encode to the same hint name: rename one)
KOIN-W001WarningModule '<name>' is not loaded at startKoin
KOIN-W003WarningGraph not verifiable at compile time:
KOIN-W004WarningCustom @Configuration label '<label>' requested by
KOIN-W005WarningDefinition '<name>' in dependency module
KOIN-W006Warning<definition>'s bind(...) argument could not be resolved
KOIN-A001Error@KoinViewModel definition '<name>' cannot be generated (add io.insert-koin:koin-core-viewmodel)
KOIN-A002Error@KoinWorker definition '<name>' cannot be generated (add io.insert-koin:koin-android-workmanager)
KOIN-A003ErrorCannot generate <Module>.module() (add io.insert-koin:koin-core)
KOIN-S001Errorcreate(::<Type>) must be the only instruction in the lambda (see unsafeDslChecks)
KOIN-P001WarningMissing @PropertyValue default:
KOIN-M001Warning@Monitor: Kotzilla SDK not found on classpath

The warnings describe what the plugin could not check. KOIN-W003, KOIN-W004 and KOIN-W005 are covered in Limits. KOIN-W006 means a bind/binds argument is not a literal X::class (or a literal array or list of them): use a literal.

External types: @Provided​

Some types reach your graph from outside Koin's declarations: a platform object, an SDK instance, a definition added by hand at runtime. Mark them @Provided so the plugin does not report them as missing:

// on a class: every use of this type is skipped
@Provided
class FirebaseAnalytics

// on a parameter: only this parameter is skipped
@Singleton
class PaymentProcessor(@Provided val gateway: PaymentGateway)

@Provided (org.koin.core.annotation) works with every declaration style: annotate the class or the constructor parameter, even when the definition is in a DSL module. It only changes the compile-time check. At runtime the type is still resolved with get(), and fails if nobody declared it. Prefer the parameter form: on a class, it turns off the check for every use of that type.

These types are always accepted, without @Provided:

  • android.content.Context
  • android.app.Application
  • android.app.Activity
  • androidx.activity.ComponentActivity
  • androidx.fragment.app.Fragment
  • androidx.lifecycle.SavedStateHandle
  • androidx.work.WorkerParameters

Modules loaded at runtime​

A module loaded with loadKoinModules(...) after startKoin is not part of the graph the plugin assembles. The plugin cannot know when, or if, that call runs. Types that only come from such a module must be @Provided:

val featureModule = module { single<Repository>() }   // loaded later
val appModule = module { single<Service>() } // Service(@Provided val repo: Repository)

startKoin { modules(appModule) }
loadKoinModules(featureModule)

The plugin still reports the runtime-loaded module as unreachable with KOIN-W001. Adding it to modules() would defeat on-demand loading, so you can ignore this warning (it fails the build only with allWarningsAsErrors).

Special parameters​

Scope: a parameter of type org.koin.core.scope.Scope receives the current scope. It is not checked, since the code resolves types from it dynamically.

@Scoped
class ScopedService(val scope: Scope) {
fun load() = scope.get<SomeDependency>()
}

@ScopeId: the parameter is resolved from another scope, by name (@ScopeId(name = "user_session")) or by type (@ScopeId(UserSessionScope::class), the scope ID is the full class name). It is not checked, since that scope exists only at runtime. See Scopes.

@Property: the parameter reads a Koin property. If no @PropertyValue("key") default is declared, the build gets a warning, and the property must be set at startup:

@PropertyValue("api.timeout")
val defaultTimeout = 30

@Factory
class ApiClient(@Property("api.timeout") val timeout: Int) // OK

@Factory
class Other(@Property("missing.key") val value: String)
// [Koin][KOIN-P001] Missing @PropertyValue default: "missing.key" ...

Default values: with skipDefaultValues = true (default), a parameter with a Kotlin default value uses it and is not checked. See skipDefaultValues.

Generic types​

Koin resolves definitions on the erased class: type arguments are not part of the lookup key. The plugin follows the same rule. get<Box<Int>>() is checked against any Box definition, and single<Box<A>>() and single<Box<B>>() are the same definition, so the second overrides the first.

To keep several instances of one generic type, give each one a type qualifier derived from its type argument with named<T>(). The pattern is described in Advanced Patterns.

Incremental builds​

The full-graph check runs when the aggregator module (the one with the entry point) compiles. Kotlin incremental compilation can skip that module after a change elsewhere, for example inside a module { } lambda body. The plugin turns on strictSafety in modules where it detects an entry point, so the check runs on every build. Details and escape hatch: strictSafety.

Limits​

  • Dynamic module lists: modules(if (debug) a else b), modules(*list) or a list built at runtime cannot be followed. The plugin reports KOIN-W003 and skips the full-graph check for that entry point. Pass modules directly to keep the check.
  • Hand-written lambdas and cycles: a single { A(get()) } lambda adds no edge to the cycle check, so a circular dependency through hand-written lambdas is not detected. Use single<T>(), singleOf(::T) or create(::T) where cycles matter.
  • Custom configuration labels: modules tagged with a label other than "default" are discovered at most one implementation dependency away (KOIN-W004). Add a direct dependency, or list the module in @KoinApplication(modules = [...]).
  • Function definitions far away: a @Module function definition, or a qualified top-level function, in a module two or more implementation hops away is not visible to the check (KOIN-W005). It still works at runtime. Add a direct dependency to check it.
  • Runtime-loaded modules: see Modules loaded at runtime.

Replacing verify() and checkModules()​

With compile-time safety on, the build checks what verify() and checkModules() tests check, on every compilation. You can remove those tests for graphs the plugin fully checks. Keep them for graphs the plugin reports as not verifiable (KOIN-W003) or that use runtime-loaded modules. See Verifying modules.