Injected Parameters
Some objects need a value that only the caller knows, such as a user ID or the current screen. Injected parameters let the caller pass these values when it asks Koin for an instance. This page covers how to declare a parameter in a definition, how to pass it, and how Koin matches values to parameters. It assumes you know how to declare a definition (see Definitions).
Declare and pass a parameter
Mark which constructor parameters come from the caller. The other parameters are resolved from the container as usual:
- Compiler Plugin DSL
- Annotations
- Classic DSL
import org.koin.core.annotation.InjectedParam
class UserPresenter(
@InjectedParam val userId: String,
val repository: UserRepository,
)
val appModule = module {
single<UserRepository>()
factory<UserPresenter>()
}
@InjectedParam comes from the koin-annotations artifact. It tells the Compiler Plugin that userId is not a definition, so the build-time check doesn't report it as missing.
@Factory
class UserPresenter(
@InjectedParam val userId: String,
val repository: UserRepository,
)
@InjectedParam works on provider function parameters too.
The definition lambda receives the parameters as params:
val appModule = module {
singleOf(::UserRepository)
factory { params -> UserPresenter(userId = params.get(), repository = get()) }
}
The caller passes the values with parametersOf(), in the same order as the parameters:
import org.koin.core.parameter.parametersOf
val presenter: UserPresenter = get { parametersOf("user-42") }
// or lazily
val presenter: UserPresenter by inject { parametersOf("user-42") }
The parameter block is a lambda, so the values are read when the instance is created. With by inject(), this is on first access.
Injected parameters are meant for factory definitions. A single is created once: the values passed the first time are used, and later values are ignored.
Each platform API takes the same block: koinInject { parametersOf(userId) } in Compose (see Koin for Compose), and by viewModel { parametersOf(userId) } or koinViewModel { parametersOf(userId) } for a ViewModel (see ViewModel).
Several parameters
- Compiler Plugin DSL
- Annotations
- Classic DSL
class OrderPresenter(
@InjectedParam val userId: String,
@InjectedParam val orderId: Long,
val repository: OrderRepository,
)
val appModule = module {
factory<OrderPresenter>()
}
@Factory
class OrderPresenter(
@InjectedParam val userId: String,
@InjectedParam val orderId: Long,
val repository: OrderRepository,
)
Destructure the parameters in the lambda:
val appModule = module {
factory { (userId: String, orderId: Long) ->
OrderPresenter(userId, orderId, get())
}
}
Or read them by index, with params[i] or params.get<T>(i):
val appModule = module {
factory { params -> OrderPresenter(params[0], params[1], get()) }
}
Destructuring is limited to five values, and the declared types are not checked against what the caller passes. If the order is wrong, the cast fails at runtime.
val presenter: OrderPresenter = get { parametersOf("user-42", 1001L) }
How Koin matches values
params.get<T>() and the generated code look up each value with these rules:
- Koin first looks at the value at the current position. If it has the requested type, Koin returns it and moves to the next position.
- Otherwise, Koin returns the first value of the requested type, wherever it is.
params.getOrNull<T>()returnsnullwhen no value of that type was passed.get<T>()throwsDefinitionParameterException.
In practice, pass the values in constructor order: parametersOf("user-42", 1001L) fills userId then orderId, and two String parameters receive two String values in order.
If you need one behavior only, build the parameters with another function:
| Function | Matching |
|---|---|
parametersOf(...) | Position first, then type (default, rules above) |
parameterArrayOf(...) | Position only. The value at the current position must have the requested type |
parameterSetOf(...) | Type only. Every get<T>() returns the first value of that type |
val byIndex = parameterArrayOf(1, 2, 3)
byIndex.get<Int>() // 1
byIndex.get<Int>() // 2
val byType = parameterSetOf("a_string", 42)
byType.get<Int>() // 42
byType.get<Int>() // 42
Values resolved from parameters
When Koin creates an instance with parameters, a plain get() inside the definition checks the passed values before the container. A parameter that is not marked can still receive a passed value:
val appModule = module {
factory { UserPresenter(userId = get(), repository = get()) }
}
get<UserPresenter> { parametersOf("user-42") } // userId = "user-42"
This also works for dependencies further down: if UserPresenter needs a Tracker that needs the screen name, a value passed for the presenter can reach the Tracker definition.
Prefer @InjectedParam or params.get() anyway: they say where the value comes from, and the Compiler Plugin can check the rest of the graph.
If a passed value has the same type as the requested definition, Koin returns that value directly and never runs the definition. get<String> { parametersOf("x") } returns "x". Wrap such parameters in their own type (a value class, for example) when they would collide with the definition's type.
A qualified request, such as get(named("local")), never takes a value from the parameters. Parameters carry no qualifier.
Related
- Injection:
get()andinject(), where you pass the parameters. - Definitions: how the other constructor parameters are resolved.
- ViewModel: parameters and
SavedStateHandlefor ViewModels. - Qualifiers: choose between definitions instead of passing values.
- Annotations Reference:
@InjectedParamand other parameter annotations.