Koin 注入参数(Injection Parameters)完全指南:定义、传递与解析
2026/9/24 14:49:16 网站建设 项目流程

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 上下文提供的值(如ViewIntent数据);
  • Compose Navigation 中上游ViewModel向下游ViewModel传递的实例;
  • 每次调用get()时希望传入不同值的factory定义。

从源码层面看,Koin 中所有定义的实际类型是:

typealias Definition<T> = Scope.(ParametersHolder) -> T

(见 BeanDefinition.kt)

也就是说,每个定义本质上都是一个接收ParametersHolder(参数持有者)的函数,注入参数正是通过这个ParametersHolder传入定义并完成解析的。

二、向定义传递参数:parametersOf

假设我们有如下定义,它依赖两个值ab来构造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 选择建议

官方文档提示:你可以用parametersOfparameterArrayOf级联(cascade)参数注入,即基于索引依次消费值;也可以用parametersOfparameterSetOf基于类型级联解析。选择哪种取决于你的传参结构是"位置敏感"还是"类型敏感"。

七、参数级联(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_paraminject_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)

也就是说:当参数类型与定义返回类型相同(例如向依赖SharedViewModelConsumerViewModel传入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,其要点可归纳为:

  1. 传递:用parametersOf()inject {}/get {}中传参;
  2. 声明:定义中可用params.get()(类型安全)或解构声明(简洁但需注意类型顺序);
  3. 解析get()按类型、get(index)/p[i]按索引,二者可由get()混用;
  4. 三种构建器parametersOf(索引+类型混合)、parameterArrayOf(纯索引)、parameterSetOf(纯类型);
  5. 级联:一组参数可沿依赖链被多个定义消费;
  6. 冲突规避:参数与定义返回类型相同时,用 value class 等包装类型隔离;
  7. 语义差异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),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询