Koin 依赖注入框架介绍
Koin 依赖注入框架介绍
1. Koin 是什么?
Koin 是一个用纯 Kotlin 写的依赖注入(DI)框架。
它的核心思想很简单:你不用在代码里到处 new 对象,而是把”对象怎么创建、创建几个、依赖谁”这些事写成一份 Kotlin DSL 配置(module)。运行时你需要什么对象,就喊一声 get(),Koin 会帮你把对象和它的依赖一起组装好。
打个比方:Koin 就像餐厅后厨的备料员 + 装配工。你提前写好一份菜单(module),写明每道菜需要什么食材、怎么搭配。客人点菜时,他不用自己切菜炒菜,只需要说”来一份番茄炒蛋”,Koin 就会按菜单把菜做好端上来。
val appModule = module {
single { Database() }
single { UserRepository(get()) }
factory { UserPresenter(get()) }
}
2. Koin 能解决什么问题?
2.1 对象创建和依赖关系太分散
没有 DI 的时候,每个类自己负责创建依赖:
class UserRepository {
private val db = Database() // 自己 new
private val api = NetworkClient() // 自己 new
}
问题:
- 换实现困难(比如把
Database换成MockDatabase要改很多地方) - 依赖关系藏在类内部,不好测试
- 单例还是多例,全靠手动控制
用了 Koin 后,类只关心”我要什么”:
class UserRepository(
private val db: Database,
private val api: NetworkClient
)
具体 Database 和 NetworkClient 怎么来、是单例还是多例,交给 Koin 统一管理。
2.2 对象生命周期管理麻烦
有些对象应该全局只有一个(比如数据库),有些应该每次新建(比如 Presenter),有些应该跟随某个页面(比如 Activity 里的 UserSession)。
Koin 用三种定义直接解决这个问题:
| 类型 | 含义 | 例子 |
|---|---|---|
single { } | 全局单例 | 数据库、网络客户端 |
factory { } | 每次新建 | Presenter、临时工具类 |
scoped { } | 某个作用域内单例 | Activity 里的 Session |
2.3 模块化和测试更友好
依赖由外部注入后,单元测试可以直接传 Mock:
val repo = UserRepository(mockDb, mockApi)
不用改业务代码,也不用搭复杂的测试环境。
3. Koin 的核心工作原理
3.1 三个核心步骤
第一步:写菜单(module)
val appModule = module {
single { Database() } // 全局只用一个
single { UserRepository(get()) } // 需要 Database
factory { UserPresenter(get()) } // 每次用都新建
}
-
single { }:全局单例 -
factory { }:每次get()都新建 -
scoped { }:某个作用域内单例 -
get():”帮我把需要的依赖拿过来”
第二步:开店(startKoin)
startKoin {
modules(appModule)
}
Koin 会把菜单读进去,建立一张”查找表”:什么类型对应哪个工厂。如果有标记 createdAtStart = true 的,还会提前实例化。
第三步:点菜(get / inject)
val repo: UserRepository = get()
Koin 一看你要 UserRepository,就去查表:
- 找到
UserRepository的工厂 - 发现它依赖
Database - 先去拿
Database - 把
Database传给UserRepository的构造函数 - 返回组装好的
UserRepository
3.2 Koin 内部怎么找依赖?
Koin 内部有一张注册表(InstanceRegistry),key 大致是 类型:qualifier:scope。get() 时:
- 先看有没有直接传参数
- 去当前 scope 的注册表里找
- 找不到就往父 scope(root scope)找
- 找到对应的
InstanceFactory,让它创建或返回缓存实例
三种工厂对应三种生命周期:
| 类型 | 内部工厂 | 行为 |
|---|---|---|
single | SingleInstanceFactory | 第一次创建,之后复用 |
factory | FactoryInstanceFactory | 每次新建 |
scoped | ScopedInstanceFactory | 按 scope 缓存,scope 关闭时销毁 |
3.3 Scope(作用域)是什么?
默认有一个 rootScope,所有 single 都存在这里。
你还可以自定义 scope,比如 Android 里的 Activity:
scope<MyActivity> {
scoped { UserSession() }
}
意思是:这个 Activity 活着,UserSession 就活着;Activity 销毁了,UserSession 也一起销毁。
自定义 scope 默认能访问 root scope 里的对象,所以全局单例在局部 scope 里也能拿到。
3.4 一个新变化:编译期验证
从 Koin 4.2 + Kotlin 2.3.20+ 开始,官方推出了 Koin Compiler Plugin(一个独立的 K2 编译器插件,不是 KAPT/KSP)。它能在编译期验证:
- 模块内依赖是否完整
- 整个 App 的依赖图是否完整
- 每个
get<T>()/inject<T>()调用点是否合法 - 循环依赖(A2/A3 阶段依赖图遍历时发现)
这补上了 Koin 原本最大的短板。现在的说法是:
经典 Koin DSL 是运行时解析;加上 Koin Compiler Plugin 后,也能做到编译期安全检查。
:::tip 使用 Compiler Plugin 时,DSL 的包名是 org.koin.plugin.module.dsl.*,与经典 DSL 的 org.koin.dsl.* 不同,且需要单独引入 io.insert-koin:koin-compiler-plugin。 :::
4. Koin 使用过程中有哪些坑?
4.1 循环依赖:经典版栈溢出,Compiler Plugin 编译期报错
经典 Koin(未启用 Compiler Plugin)
Koin 核心引擎没有运行时的循环依赖检测。如果写成:
single { ServiceA(get()) } // A 依赖 B
single { ServiceB(get()) } // B 依赖 A
运行时创建 A 会去找 B,创建 B 又会去找 A,无限递归,最终抛出 StackOverflowError。
源码层面也能印证:koin-core 的 InstanceFactory/SingleInstanceFactory 在 get() 时只是检查实例是否存在,不存在就递归 create(),没有图遍历的环检测逻辑。
启用 Koin Compiler Plugin 后
Compiler Plugin 在 A2(模块级)和 A3(全图级)阶段会做依赖图遍历,能在编译期直接检测循环依赖并报错,而不是等到运行时才崩溃。
官方文档明确说明:
Circular dependency (A → B → A) — ERROR — detected during A2/A3 graph traversal.
但注意:Compiler Plugin 是”检测”不是”自动修复”。报错后,你还是需要手动打破循环。常用方案:
- 懒加载注入:把其中一个依赖改成
Lazy<T>,让对象先完成构造,再在首次使用时解析依赖(注意:单纯by inject()只是把第一次get()推迟到首次访问,若构造期已经互相get()仍可能触发循环)。 - 提取公共依赖:把双向依赖改成共同依赖一个第三方对象(推荐)。
- 接口 + 懒加载/共同依赖:引入接口可以削弱耦合,但仅换接口并不能消除循环,仍需配合懒加载或公共依赖才能真正打破循环。
解决建议:新项目如果环境允许(Kotlin 2.3.20+),直接启用 Koin Compiler Plugin,既能编译期发现循环依赖,也能提前发现缺依赖、Qualifier 对不上等问题。
4.2 缺少依赖在运行时才报错
没有 Compiler Plugin 的情况下,如果你忘了注册某个类型:
val appModule = module {
single { UserRepository(get()) } // 需要 Database,但 Database 没注册
}
App 启动或第一次调用 get<UserRepository>() 时才会抛 NoDefinitionFoundException。
解决:升级到 Koin Compiler Plugin,让编译期就报错;或者写 module.verify() 测试兜底(checkModules() 在 Koin 4.x 中已标记为废弃,建议迁移到 verify())。
4.3 single 和 factory 用错导致诡异 Bug
- 本该全局单例的用了
factory:每次用都新建,状态不共享 - 本该每次新建的用了
single:多线程下共享可变状态,容易出并发问题
解决:想清楚对象的生命周期,记不住就默念:”数据库 single,Presenter factory”。
4.4 Scope 关闭时机没管好
自定义 scope 用完要手动 close(),否则 scoped 实例会一直占着内存:
val scope = koin.createScope<MyActivity>(scopeId)
// Activity 销毁时记得
scope.close()
在 Android 上通常用 koin-android 的 activityScope() / fragmentScope() 自动管理。
4.5 Qualifier 写错很难发现
single(named("local")) { Database() }
single { UserRepository(get()) } // 这里没写 named("local"),会找不到
没有 Compiler Plugin 时,这种错误运行时才暴露。
解决:统一常量管理 qualifier,或开启 Compiler Plugin。
4.6 多模块项目里的覆盖问题
Koin 默认允许覆盖定义。如果不小心两个模块注册了同一个类型,后加载的会覆盖先加载的:
single { RealRepository() }
single { FakeRepository() } // 覆盖了上面的
解决:显式设置 allowOverride(false) 或 strictOverride(),让覆盖直接报错。
5. Koin 与 Hilt 对比的优势和劣势
| 维度 | Koin | Hilt(Dagger) |
|---|---|---|
| 实现方式 | 运行时通过 DSL 配置 + 查找 | 编译期生成代码 |
| 出错时机 | 运行时(经典 DSL)/ 编译期(有 Compiler Plugin) | 编译期 |
| 学习成本 | 低:纯 Kotlin DSL,不用理解注解处理器 | 高:需要理解 @Inject、@Module、@Provides、@Singleton、@ActivityScoped 等概念 |
| 代码量 | 少,配置即代码 | 多,需要写 Module 和 Provider |
| 性能 | 启动时多一点点 HashMap 查找和同步开销;singleOf/new 等现代 DSL 对象创建不依赖反射 | 运行时几乎没有额外开销 |
| 调试难度 | 依赖查找链路在运行时,有时堆栈较深 | 生成代码可阅读,问题通常编译期暴露 |
| 与 Android 集成 | 需要手动管理 Activity/Fragment scope | 深度集成 Android 生命周期,自动管理 |
| 编译速度 | 经典 DSL 无注解处理,编译快;启用 Compiler Plugin 后由 K2 插件在编译期分析,通常仍快于 KAPT | KAPT 慢,Hilt 用 KSP 后有改善 |
| 生态成熟度 | 社区活跃,Google 官方推荐度不如 Hilt | Google 官方推荐,生态更成熟 |
| 编译期安全 | 经典版弱;新版 Compiler Plugin 强 | 一直很强 |
5.1 Koin 的优势
- 上手快:会写 Kotlin 就会写 Koin,几行代码就能跑起来。
- 配置即代码:module 就是普通 Kotlin 文件,可以用变量、循环、条件判断。
- 经典 DSL 无需注解处理器:纯手写 DSL 时不依赖 KAPT/KSP;若启用 Compiler Plugin,则它会在 K2 编译阶段工作(同样不是 KAPT/KSP)。
- 跨平台:支持 Kotlin Multiplatform、Ktor、Compose Multiplatform 等。
- 灵活性高:可以运行时动态加载模块、替换定义。
5.2 Koin 的劣势
- 运行时开销:每次
get()都有一次 HashMap 查找(O(1)),复杂依赖链会累积多次查找与同步锁;对大型项目启动时可能有影响,但通常不是反射开销。 - 错误发现晚:经典 DSL 下,缺依赖要到运行时才报错。
- Android 生命周期集成弱:相比 Hilt,Activity/Fragment/ViewModel 的作用域需要额外配置。
- 生态不如 Hilt:Google 官方文档和示例更多偏向 Hilt。
5.3 一句话对比
Hilt 是”先验安全”:编译期把所有问题查清楚,适合大型、多人协作的 Android 项目。
Koin 是”快速灵活”:写起来爽、跨平台、好上手;在 Kotlin 2.3.20+ 环境下启用 Compiler Plugin 后,编译期安全也能得到补齐。
一句话总结
Koin 是一个轻量、灵活的 Kotlin 依赖注入框架:启动时你把”对象配方”(module)告诉它,运行时它按配方把依赖一件一件找出来、组装好交给你。在 Kotlin 2.3.20+ 项目中启用 Koin Compiler Plugin 后,还能在编译期提前发现依赖问题。
Enjoy Reading This Article?
Here are some more articles you might like to read next: