Koin 注入参数(Injection Parameters)完全指南:定义、传递与解析
【免费下载链接】koinKoin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform项目地址: https://gitcode.com/gh_mirrors/ko/koin
本文基于 Koin 开源仓库 docs/reference/koin-core/injection-parameters.md 编写,并辅以 koin-core 源码与测试验证。注入参数(Injection Parameters)是 Koin 在运行时向组件定义(definition)传递动态值的核心机制,广泛应用于传参构建对象、ViewModel 实例传递、工厂按需创建等场景。读完本文,你将掌握
parametersOf/parameterArrayOf/parameterSetOf三种参数构建方式、按索引与按类型解析参数、级联传参,以及参数与定义类型冲突时的规避方案。
一、什么是注入参数
在 Koin 中,任何一个组件定义(definition)都可以接收注入参数(injection parameters):这些参数会被注入到定义中,供该定义在构建实例时使用。与依赖图(dependency graph)中静态解析的依赖不同,注入参数是由调用方在请求实例时动态传入的,因此非常适合传递"只有运行时才知道"的值,例如:
- Activity / Fragment 中由 UI 上下文提供的值(如
View、Intent数据); - Compose Navigation 中上游
ViewModel向下游ViewModel传递的实例; - 每次调用
get()时希望传入不同值的factory定义。
从源码层面看,Koin 中所有定义的实际类型是:
typealias Definition<T> = Scope.(ParametersHolder) -> T(见 BeanDefinition.kt)
也就是说,每个定义本质上都是一个接收ParametersHolder(参数持有者)的函数,注入参数正是通过这个ParametersHolder传入定义并完成解析的。
二、向定义传递参数:parametersOf
假设我们有如下定义,它依赖两个值a、b来构造Presenter:
class Presenter(val a : A, val b : B) val myModule = module { single { params -> Presenter(a = params.get(), b = params.get()) } }在定义内部,params就是ParametersHolder,通过params.get<T>()可以按类型取出传入的参数。
参数通过parametersOf()函数发送给定义,每个值用逗号分隔:
class MyComponent : View, KoinComponent { val a : A ... val b : B ... // inject this as View value val presenter : Presenter by inject { parametersOf(a, b) } }在仓库测试 ParametersInjectionTest.kt 中有对应的完整验证:
val app = koinApplication { modules( module { single { params -> Simple.MySingle(params.get()) } }, ) } val koin = app.koin val a: Simple.MySingle = koin.get { parametersOf(42) } assertEquals(42, a.id)除了by inject { parametersOf(...) }(用于KoinComponent或属性委托),你同样可以在任意位置通过koin.get<T> { parametersOf(...) }或scope.get<T> { parametersOf(...) }显式携带参数请求实例。
三、在定义中声明"注入参数"
3.1 使用 params.get() 获取参数
假如我们需要一个view参数来构建Presenter,可以使用定义函数的params参数来检索注入参数:
class Presenter(val view : View) val myModule = module { single { params -> Presenter(view = params.get()) } }3.2 使用解构声明直接书写参数
Koin 也支持把注入参数直接写进定义的 lambda 参数位置,作为解构声明(destructured declaration):
class Presenter(val view : View) val myModule = module { single { (view : View) -> Presenter(view) } }从源码看,ParametersHolder实现了component1()~component5()操作符(见 ParametersHolder.kt),解构声明正是通过它们按位置(0 到 4)取出参数:
inline operator fun <reified T> component1(): T = elementAt(0, T::class) inline operator fun <reified T> component2(): T = elementAt(1, T::class) // ... component3 / component4 / component5仓库测试 ParametersInjectionTest.kt 中can create a single with parameters in order - destruct即验证了解构方式:
module { single { (a: Int, b: Int) -> Simple.MyTwinSingle(a, b) } } val a: Simple.MyTwinSingle = koin.get { parametersOf(42, 24) } assertEquals(42, a.i1) assertEquals(24, a.i2)⚠️ 注意(官方文档警示):即便"解构声明"写法更简洁、可读性更好,但它不是类型安全的。当存在多个参数时,Kotlin 无法检测你传入的类型顺序是否正确。例如
(a: Int, b: String)解构时,如果调用方按parametersOf("x", 42)传入,Kotlin 编译器不会报错,运行时才会出问题。因此在参数较多、类型容易混淆的场景下,优先使用params.get<T>()按类型解析。
四、按索引解析注入参数:get(index) 与 [ ]
如果不使用get()按类型解析,而同一类型存在多个参数,你可以用索引来解析:get(index),等价于[ ]操作符:
class Presenter(val view : View) val myModule = module { single { p -> Presenter(p[0],p[1]) } }ParametersHolder源码中索引访问的实现(ParametersHolder.kt):
operator fun <T> get(i: Int) = _values[i] as T测试 ParametersInjectionTest.kt 中can create a single with parameters in order验证了这一写法:
module { single { p -> Simple.MyTwinSingle(p[0], p[1]) } } val a: Simple.MyTwinSingle = koin.get { parametersOf(42, 24) } assertEquals(42, a.i1) assertEquals(24, a.i2)注意:get(index)越界时,elementAt会抛出NoParameterFoundException,提示无法从参数持有者中取到第 N 个参数(见 ParametersHolder.kt)。
五、从依赖图中解析注入参数:直接 get()
Koin 的图解析(所有定义的解析主树)也允许你直接解析注入参数——只需在定义中使用常规的get()函数:
class Presenter(val view : View) val myModule = module { single { Presenter(get()) } }此时get()会先从注入参数栈中查找匹配类型的值,找到则直接使用;找不到再回退到依赖图(registry)解析。仓库测试can create a single with parameters - using graph resolution(ParametersInjectionTest.kt)证明:koin.get { parametersOf(42) }时,定义内get()拿到的是注入的42而非图中的其他定义。
更复杂的场景——多个定义同时使用图解析并配合named限定符:
module { single { Simple.MySingle(get()) } single(named("2")) { Simple.MySingle(get()) } } assertEquals(42, koin.get<Simple.MySingle> { parametersOf(42) }.id) assertEquals(24, koin.get<Simple.MySingle>(named("2")) { parametersOf(24) }.id)六、三种参数构建方式:parametersOf / parameterArrayOf / parameterSetOf
在parametersOf之外,Koin(自 3.4.3 起)还提供了两种专用参数构建 API。三者都返回ParametersHolder,区别在于内部消费方式(对应源码 ParametersHolder.kt):
| 构建函数 | 底层useIndexedValues | 消费方式 | 适用场景 |
|---|---|---|---|
parametersOf(vararg) | null(索引优先,回退类型) | 先按索引消费,失败则按类型匹配首个值 | 通用场景,两种解析兼得 |
parameterArrayOf(vararg) | true | 严格按索引依次消费 | 级联传参、多个同类型参数 |
parameterSetOf(vararg) | false | 严格按类型匹配,不使用索引 | 传递不同类型值的集合 |
6.1 parameterArrayOf:按索引消费的数组
parameterArrayOf用于传一个数组值,数据按索引被消费:
val params = parameterArrayOf(1,2,3) params.get<Int>() == 1 params.get<Int>() == 2 params.get<Int>() == 3 params.get<Int>() == 3注意最后一行:当索引到达最后一个元素后,increaseIndex()不会继续自增(见 ParametersHolder.kt),因此重复调用get<Int>()会停留在3。
测试 CascadeParamTest.kt 中的parameter_array精确验证了这一行为(含索引游标变化):
val p = parameterArrayOf(intParam, stringParam) assertEquals(0, p.index) assertNull(p.getOrNull<String>()) // 索引0是 Int,类型不匹配 → null,索引不动 assertEquals(intParam, p.get<Int>()) // 索引0匹配 → 消费并自增 assertEquals(stringParam, p.get<String>()) // 索引1匹配 → 消费 assertEquals(1, p.index)6.2 parameterSetOf:按类型匹配的集合
parameterSetOf用于传一组不同类型的值,不使用索引滚动值,而是每次按请求的类型匹配:
val params = parameterSetOf("a_string", 42) params.get<Int>() == 42 params.get<String>() == "a_string" params.get<Int>() == 42 params.get<String>() == "a_string"测试parameter_set(CascadeParamTest.kt)验证:无论请求多少次、按什么顺序,parameterSetOf都通过getFirstValue按类型找到匹配值,索引始终保持为 0。
6.3 parametersOf:索引与类型的混合模式
默认的parametersOf同时兼容索引与类型两种解析:当useIndexedValues == null时,getOrNull(clazz)会先尝试取当前索引处的值(类型需匹配),失败则回退到按类型匹配首个值(源码 ParametersHolder.kt):
when (useIndexedValues) { null -> getIndexedValue<T>(clazz) ?: getFirstValue<T>(clazz) true -> getIndexedValue<T>(clazz) else -> getFirstValue<T>(clazz) }因此文档中的示例成立:
val params = parametersOf(1,2,"a_string") params.get<String>() == "a_string" params.get<Int>() == 1 params.get<Int>() == 2 params.get<Int>() == 2 params.get<String>() == "a_string"其消费过程可以这样理解:请求String时,索引 0 处是Int不匹配,回退按类型找到"a_string";请求Int时索引 0 匹配1,消费并推进索引到 1;再次请求Int命中2;再请求Int时索引停在2(最后一个元素);最后请求String回退按类型找到"a_string"。仓库测试consume_bad_value(CascadeParamTest.kt)正是对这一混合行为的回归验证。
6.4 选择建议
官方文档提示:你可以用
parametersOf或parameterArrayOf级联(cascade)参数注入,即基于索引依次消费值;也可以用parametersOf或parameterSetOf基于类型级联解析。选择哪种取决于你的传参结构是"位置敏感"还是"类型敏感"。
七、参数级联(Cascade):多级定义共享同一组参数
级联注入是注入参数最强大的应用之一:一次传入的一组参数,可以沿着依赖链被多个定义依次消费。
例如 CascadeParamTest.kt 中can_cascade_param_full_ctor_dsl:
factoryOf(Simple::MyIntFactory) factoryOf(Simple::MyStringFactory) factoryOf(Simple::AllFactory) val allFactory = koin.get<Simple.AllFactory> { parameterArrayOf(intParam, stringParam) } assertEquals(intParam, allFactory.ints.id) assertEquals(stringParam, allFactory.strings.s)AllFactory依赖MyIntFactory(消费Int参数)和MyStringFactory(消费String参数),parameterArrayOf传入的(42, "_string_")被依次级联消费。同样地,ParametersInjectionTest.kt 中chained factory injection展示了对子定义显式重传参数的方式:
factory { (i: Int) -> Simple.MyIntFactory(i) } factory { (s: String) -> Simple.MyStringFactory(s) } factory { (i: Int, s: String) -> Simple.AllFactory( get { parametersOf(i) }, get { parametersOf(s) }, ) }如果子定义本身需要携带不同参数,可以在get { parametersOf(...) }中重新指定;如果子定义没有特殊参数需求,则直接使用图解析get(),参数会自动级联。
八、可空参数与安全解析:get / getOrNull / getOrNull()
8.1 可空参数
注入参数本身可以是null。测试can create a single with nullable parameters验证:
single { (i: Int?) -> Simple.MySingleWithNull(i) } val a: Simple.MySingleWithNull = koin.get { parametersOf(null) } assertEquals(null, a.id)8.2 getOrNull 安全解析
当参数可能缺失时,使用getOrNull()避免异常。测试nullable_injection_param与inject_param_get_or_null(ParametersInjectionTest.kt):
single { p -> Simple.MySingleWithNull(p.getOrNull()) } val a: Simple.MySingleWithNull = koin.get() // 不传参数也不抛异常 assertNull(a.id)注意:get<T>()在找不到匹配类型时会抛出DefinitionParameterException("No value found for type ..."),而getOrNull()返回null(源码见 ParametersHolder.kt)。另外getOrNull也支持传KClass,如p.getOrNull<String>(String::class)。
8.3 类型匹配的细节
getFirstValue使用clazz.isInstance(it)判断(ParametersHolder.kt),因此父接口类型可以匹配到子类实例。测试assignable type values验证了这一点:
val p = parametersOf(Simple.Component1()) assertNotNull(p.get<Simple.ComponentInterface1>())九、参数与定义类型冲突:必须用包装类型隔离
⚠️ 官方文档警告:如果通过
parametersOf传入的某个值与所请求定义具有相同类型,Koin 会直接返回该值本身并跳过 factory 构建块。为避免这种冲突,参数应使用包装类型(如 value class)隔离。
这一点在仓库测试injected parameter instance should be used directly - not resolved from registry(ParametersInjectionTest.kt)中有明确的回归验证(对应 issue #2337):
// SharedViewModel 有定义且需要 Int 参数 single { (id: Int) -> SharedViewModel(id) } factory { ConsumerViewModel(get()) } // 传入已存在的 SharedViewModel 实例,应直接使用该实例,而不是从注册表重建 val consumer: ConsumerViewModel = koin.get { parametersOf(existingSharedVM) } assertEquals(existingSharedVM, consumer.shared)也就是说:当参数类型与定义返回类型相同(例如向依赖SharedViewModel的ConsumerViewModel传入SharedViewModel实例)时,Koin 会"短路"——直接使用传入实例,跳过 factory 逻辑。这是注入参数优先于注册表解析的有意设计,但反过来也意味着:如果你希望参数与定义返回类型相同,却又想让 factory 块真正执行,就必须引入包装类型:
@JvmInline value class ViewModelParam(val viewModel: SharedViewModel) single { (p: ViewModelParam) -> ConsumerViewModel(p.viewModel) }顺带一提,仓库 QualifierParameterShadowingTest.kt 记录了一个已知边界行为:参数栈按类型(忽略限定符)优先匹配,因此与parametersOf传入值同类型的get(named(...))依赖可能被参数遮蔽——在设计 API 时应注意避免限定符依赖与传入参数类型重叠。
十、ParametersHolder 的更多实用操作
ParametersHolder除了解析,还支持运行时修改参数集合(源码 ParametersHolder.kt):
| API | 说明 |
|---|---|
size()/isEmpty()/isNotEmpty() | 参数数量与空判断 |
get(i)/set(i, t) | 按索引读写(set支持替换值) |
insert(index, value) | 在指定位置插入参数,返回自身 |
add(value) | 追加参数,返回自身 |
get<T>()/getOrNull<T>()/getOrNull(clazz) | 按类型解析(见上文) |
component1()~component5() | 供解构声明使用 |
测试 ParametersHolderTest.kt 对这些操作均有覆盖,例如:
val params = parametersOf("empty", 42) val newParams = params.insert(0, myInt) assertEquals(3, newParams.size()) assertEquals(newParams.get<Int>(0), myInt)十一、single 与 factory 的参数语义差异
注入参数与定义的生命周期语义需要区分清楚:
single(单例):参数只在首次创建实例时生效。测试can get a single created with parameters - no need of give it again证明:首次koin.get { parametersOf(42) }创建后,后续koin.get()不带参数也能拿到同一实例(id 仍为 42)。factory(工厂):每次get()都会用传入的参数重新创建新实例。测试can create factories with params验证:get { parametersOf(42) }与get { parametersOf(43) }会得到两个不同 id 的实例。
因此,动态性要求高的场景(如每次请求不同配置)应使用factory,而固定的运行时参数交给single首次注入即可。
十二、综合实战示例
结合以上全部能力,一个完整的多参数、多模式注入示例:
// 1. 按类型解析 + 图解析混用 single { params -> Presenter(view = params.get(), repo = get()) } // 2. 多个同类型参数按索引解析 single { p -> TwinPresenter(p[0], p[1]) } // 3. 解构声明(注意类型顺序风险) single { (view: View, id: Long) -> ScopedPresenter(view, id) } // 4. 请求时传参(KoinComponent 委托注入) class MyScreen : KoinComponent { val presenter: Presenter by inject { parametersOf(view, repository) } } // 5. 直接调用传参(级联给子定义) val presenter: Presenter = koin.get { parameterArrayOf(view, repository) } // 6. 类型集合传参 val presenter: Presenter = koin.get { parameterSetOf(view, repository, 42L) }对应模块组装:
val app = koinApplication { modules( module { single { params -> Presenter(view = params.get(), repo = get()) } single { p -> TwinPresenter(p[0], p[1]) } factory { (view: View, id: Long) -> ScopedPresenter(view, id) } }, ) }十三、小结
注入参数是 Koin 面向运行时动态传参的核心 API,其要点可归纳为:
- 传递:用
parametersOf()在inject {}/get {}中传参; - 声明:定义中可用
params.get()(类型安全)或解构声明(简洁但需注意类型顺序); - 解析:
get()按类型、get(index)/p[i]按索引,二者可由get()混用; - 三种构建器:
parametersOf(索引+类型混合)、parameterArrayOf(纯索引)、parameterSetOf(纯类型); - 级联:一组参数可沿依赖链被多个定义消费;
- 冲突规避:参数与定义返回类型相同时,用 value class 等包装类型隔离;
- 语义差异:
single参数只在首次创建生效,factory每次重建。
如需深入验证,可继续阅读仓库中的 ParametersHolder.kt(参数持有者实现)、ParametersInjectionTest.kt 与 CascadeParamTest.kt(行为验证),以及 definitions.md(定义 DSL 总览)与 injection.md(注入方式概览)。
【免费下载链接】koinKoin - a pragmatic lightweight dependency injection framework for Kotlin & Kotlin Multiplatform项目地址: https://gitcode.com/gh_mirrors/ko/koin
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考