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
)

具体 DatabaseNetworkClient 怎么来、是单例还是多例,交给 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,就去查表:

  1. 找到 UserRepository 的工厂
  2. 发现它依赖 Database
  3. 先去拿 Database
  4. Database 传给 UserRepository 的构造函数
  5. 返回组装好的 UserRepository

3.2 Koin 内部怎么找依赖?

Koin 内部有一张注册表(InstanceRegistry),key 大致是 类型:qualifier:scopeget() 时:

  1. 先看有没有直接传参数
  2. 去当前 scope 的注册表里找
  3. 找不到就往父 scope(root scope)找
  4. 找到对应的 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-coreInstanceFactory/SingleInstanceFactoryget() 时只是检查实例是否存在,不存在就递归 create(),没有图遍历的环检测逻辑。

启用 Koin Compiler Plugin 后

Compiler Plugin 在 A2(模块级)和 A3(全图级)阶段会做依赖图遍历,能在编译期直接检测循环依赖并报错,而不是等到运行时才崩溃。

官方文档明确说明:

Circular dependency (A → B → A) — ERROR — detected during A2/A3 graph traversal.

但注意:Compiler Plugin 是”检测”不是”自动修复”。报错后,你还是需要手动打破循环。常用方案:

  1. 懒加载注入:把其中一个依赖改成 Lazy<T>,让对象先完成构造,再在首次使用时解析依赖(注意:单纯 by inject() 只是把第一次 get() 推迟到首次访问,若构造期已经互相 get() 仍可能触发循环)。
  2. 提取公共依赖:把双向依赖改成共同依赖一个第三方对象(推荐)。
  3. 接口 + 懒加载/共同依赖:引入接口可以削弱耦合,但仅换接口并不能消除循环,仍需配合懒加载或公共依赖才能真正打破循环。

解决建议:新项目如果环境允许(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 singlefactory 用错导致诡异 Bug

  • 本该全局单例的用了 factory:每次用都新建,状态不共享
  • 本该每次新建的用了 single:多线程下共享可变状态,容易出并发问题

解决:想清楚对象的生命周期,记不住就默念:”数据库 single,Presenter factory”。

4.4 Scope 关闭时机没管好

自定义 scope 用完要手动 close(),否则 scoped 实例会一直占着内存:

val scope = koin.createScope<MyActivity>(scopeId)
// Activity 销毁时记得
scope.close()

在 Android 上通常用 koin-androidactivityScope() / 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 的优势

  1. 上手快:会写 Kotlin 就会写 Koin,几行代码就能跑起来。
  2. 配置即代码:module 就是普通 Kotlin 文件,可以用变量、循环、条件判断。
  3. 经典 DSL 无需注解处理器:纯手写 DSL 时不依赖 KAPT/KSP;若启用 Compiler Plugin,则它会在 K2 编译阶段工作(同样不是 KAPT/KSP)。
  4. 跨平台:支持 Kotlin Multiplatform、Ktor、Compose Multiplatform 等。
  5. 灵活性高:可以运行时动态加载模块、替换定义。

5.2 Koin 的劣势

  1. 运行时开销:每次 get() 都有一次 HashMap 查找(O(1)),复杂依赖链会累积多次查找与同步锁;对大型项目启动时可能有影响,但通常不是反射开销。
  2. 错误发现晚:经典 DSL 下,缺依赖要到运行时才报错。
  3. Android 生命周期集成弱:相比 Hilt,Activity/Fragment/ViewModel 的作用域需要额外配置。
  4. 生态不如 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:

  • 搞懂 GC Root
  • Now in Android 的 Hilt 组件依赖图
  • 理解 Android 的 baseline-prof.txt 和 startup-prof.txt
  • Hilt / Koin / Knit 对比分析
  • Android 官方 Skills 分析报告