Android 项目里,
Activity需要ViewModel,ViewModel需要Repository,Repository又需要网络接口和数据库。项目小的时候,在类里直接new或调用构造函数很方便;项目变大后,这些创建动作会散落在页面、模块和测试里。换一个实现、控制对象的生命周期或编写单元测试,都会变得麻烦。
依赖注入(Dependency Injection,DI)解决的核心问题是:**对象声明自己需要什么,由外部负责提供这些依赖。**本文讲清 Android 开发里常见的 DI 库、它们如何工作、适合什么场景,以及"现在主流是哪个"应该怎样回答。
先划定范围:**Hilt 是面向 Android 的 DI 框架;Dagger、Koin、Kodein-DI 等本身并不只服务 Android,但有 Android 集成能力。**把它们一起讨论,是因为实际选型时开发者会在这些方案之间比较。
一、先用一个例子理解 DI
没有 DI 时,ViewModel 自己创建数据源:
kotlin
class UserViewModel : ViewModel() {
private val repository = DefaultUserRepository(
api = RealUserApi(),
)
}
这段代码把 UserViewModel 绑在具体实现上。测试时想用假数据源、线上想替换 API、不同构建变体想切换实现,都要改它或绕过它。
改成构造函数注入:
kotlin
class UserViewModel(
private val repository: UserRepository,
) : ViewModel()
interface UserRepository {
suspend fun loadUser(id: String): User
}
现在 UserViewModel 只表达"我需要一个 UserRepository",不负责决定它从哪里来。测试可以传入 FakeUserRepository;正式应用可以传入 DefaultUserRepository。这种设计已经是 DI,使用框架并不是前提。
手工创建这些对象通常称为手动 DI。依赖越来越多后,可以引入容器或代码生成器负责组装。
二、所有 DI 库都在处理四件事
理解框架之前,先记住四个概念:
| 概念 | 问题 | Android 例子 |
|---|---|---|
| Binding(绑定) | 需要 UserRepository 时给哪个实现? |
UserRepository → DefaultUserRepository |
| Graph(依赖图) | 创建一个对象要先创建哪些对象? | ViewModel → Repository → API/DAO |
| Scope(作用域) | 对象复用多久? | 应用级、Activity 级、ViewModel 级 |
| Qualifier(限定符) | 同一种类型有多个实现时选哪个? | 两个不同的 OkHttpClient |
依赖图是一张有方向的关系图:
text
UserViewModel
└── UserRepository(接口)
└── DefaultUserRepository(实现)
├── UserApi
└── UserDao
框架的主要区别不是"有没有依赖图",而是什么时候检查这张图、如何创建对象、怎样把作用域接入 Android 生命周期。
三、常见方案先看总览
| 方案 | 核心机制 | Android 集成 | 更合适的场景 |
|---|---|---|---|
| 手动 DI | 构造函数和自行维护的容器 | 自己管理 Application/页面作用域 |
小项目、学习原理、依赖关系很少的模块 |
| Hilt | 基于 Dagger,编译期生成依赖图和 Android 组件 | 对 Application、Activity、Fragment、ViewModel 等提供约定 |
以 Jetpack 为主的中大型原生 Android 项目 |
| Dagger | 注解处理生成类型安全的依赖图 | 需要自己定义组件;旧项目可能使用 dagger.android |
需要高度定制依赖图、已有 Dagger 基础设施 |
| Koin | Kotlin DSL 注册定义,运行时由容器解析 | 提供 Android、ViewModel、Compose 等扩展 | 重视接入速度、Kotlin 风格、跨平台共享的团队 |
| Kodein-DI | Kotlin DSL 容器、绑定与作用域 | 提供 Android 相关扩展 | 已有 Kodein 项目或偏好显式容器 API |
| Toothpick | 运行时作用域树配合注入与代码生成 | 可管理 Android 对象作用域 | 主要见于存量项目,新增项目先评估维护状态 |
这张表不表示"所有项目必须引入一个库"。如果只有几层依赖,手动 DI 常常足够;如果需要跨大量模块管理生命周期和测试替换,框架才更有价值。
四、Hilt:Android 项目里的默认优先选项
Hilt 由 Google 围绕 Android 场景构建,底层依赖 Dagger。它把原本需要手写的大量 Dagger Component 与 Android 注入入口,封装成一套约定:标记入口、声明绑定、选择组件作用域,剩下的连接代码在编译期生成。
Hilt 的工作流程
text
@Inject / @Provides / @Binds 声明"怎么提供对象"
↓
@InstallIn 声明"绑定进入哪个组件"
↓
Hilt + Dagger 在编译期构建并校验依赖图
↓
生成工厂、组件和 Android 入口连接代码
↓
应用运行时沿生成的图创建或复用对象
例如某个 ViewModel 需要仓库接口:
kotlin
interface UserRepository {
fun displayName(): String
}
class DefaultUserRepository @Inject constructor() : UserRepository {
override fun displayName() = "Ada"
}
@Module
@InstallIn(SingletonComponent::class)
abstract class RepositoryModule {
@Binds
@Singleton
abstract fun bindUserRepository(
impl: DefaultUserRepository,
): UserRepository
}
@HiltViewModel
class UserViewModel @Inject constructor(
private val repository: UserRepository,
) : ViewModel() {
val name: String = repository.displayName()
}
@HiltAndroidApp
class DemoApp : Application()
@AndroidEntryPoint
class MainActivity : ComponentActivity() {
private val viewModel: UserViewModel by viewModels()
}
示例省略了 import 和 Gradle 插件配置;实际项目应按官方文档添加与当前 Kotlin/AGP 兼容的 Hilt 版本。上面的 @Binds 告诉依赖图:请求 UserRepository 时,使用 DefaultUserRepository。@Inject constructor 让 Dagger 知道如何创建具体类。若提供对象需要调用第三方构建器或工厂,例如 Retrofit、Room 数据库,通常使用 @Provides。
为什么 @InstallIn 和 Scope 必须一起理解
Hilt 有与 Android 生命周期对应的组件,例如 SingletonComponent、ActivityRetainedComponent、ViewModelComponent、ActivityComponent、FragmentComponent 等。绑定装入哪个组件,决定它在什么地方可见;是否加 @Singleton、@ActivityScoped、@ViewModelScoped 等 Scope,则影响对象在该组件内是否复用。
一个常见误解是"注入的对象都是单例"。未加 Scope 的绑定,通常每次请求都可以得到新实例。 @Singleton 是与应用组件关联的复用范围,不代表对象永远不会被销毁;进程结束后它仍会消失。
另一个误解是把 Activity 或 View 放进应用级单例。应用级对象活得比页面久,若长期持有页面引用,容易造成泄漏。需要 Context 时要明确是 Application Context,还是只在页面生命周期内使用的对象。
Hilt 适合什么
- 原生 Android 项目以 Jetpack ViewModel、Navigation、WorkManager、Compose 等技术为主;
- 模块多,Repository、数据库、网络客户端和用例的关系逐渐复杂;
- 希望在编译期发现缺失绑定、循环依赖和类型不匹配;
- 希望测试时替换整个模块或某个依赖;
- 团队希望沿用 Android 官方文档和示例的默认路径。
代价是 Gradle 插件与代码生成的配置、增量编译成本,以及必须理解组件层级。把每个类都注入、每个对象都设为 @Singleton,只会让图更复杂。
五、Dagger:Hilt 的底座,也是更底层的控制选项
Dagger 本身是通用的编译期 DI 框架:通过 @Inject、@Module、@Provides、@Binds、@Component 等声明依赖关系,编译器生成 Java/Kotlin 可调用的工厂与组件代码。它不依赖运行时反射去猜构造函数,因此图中常见的缺失绑定和环会在构建时暴露。
与 Hilt 相比,直接使用 Dagger 时需要自己设计 Component/Subcomponent、入口、作用域,以及它们如何跟 Activity、Fragment、ViewModel 的生命周期连接。自由度高,也意味着更多样板代码和维护责任。
适用场景:
- 既有项目已经稳定使用 Dagger,迁移 Hilt 的收益不明显;
- 依赖图有 Hilt 约定不易表达的特殊组件边界;
- Android 与非 Android 的 JVM 模块共享 Dagger 图;
- 团队需要精确控制组件创建和依赖传入。
旧项目里可能看到 dagger.android、@ContributesAndroidInjector。它们是 Dagger 的 Android 辅助集成,理解存量代码仍有必要;新建 Jetpack Android 项目通常先看 Hilt,不必从这些旧式入口开始。
六、Koin:Kotlin DSL 与运行时容器
Koin 的入门路径更短。常见写法是用 Kotlin DSL 声明对象如何创建,然后在运行时通过容器解析:
kotlin
interface UserRepository {
fun displayName(): String
}
class DefaultUserRepository : UserRepository {
override fun displayName() = "Ada"
}
class UserViewModel(
private val repository: UserRepository,
) : ViewModel() {
val name = repository.displayName()
}
val appModule = module {
single<UserRepository> { DefaultUserRepository() }
viewModel { UserViewModel(get()) }
}
class DemoApp : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@DemoApp)
modules(appModule)
}
}
}
示例省略 import 和依赖配置。single 表示容器内复用一个实例,factory 通常表示每次解析创建实例,viewModel 会接入 Android 的 ViewModelStoreOwner 生命周期。模块可以按业务拆分,再组合到应用入口。
Koin 常规 DSL 不要求先跑 Dagger 式注解处理器,接入和改动比较直接。代价是部分图错误可能要到运行时解析依赖时才出现,例如 get() 请求的绑定没有注册。项目应在测试或启动检查中验证模块,并为关键路径编写集成测试。Koin 也提供注解与辅助校验方案;因此不能简单把"所有 Koin 项目都没有编译期工具"当成绝对结论。
Koin 适合:
- 中小型 Android 项目,或需要快速拆分模块的应用;
- 团队喜欢 Kotlin DSL、希望减少代码生成配置;
- Kotlin Multiplatform 项目想共享业务层依赖定义,同时给 Android/iOS 分别接入平台实现;
- 已经有成熟 Koin 模块和测试约定的团队。
七、Kodein-DI 与 Toothpick:什么时候会遇到
Kodein-DI 也是 Kotlin 风格的 DI 容器,通过模块、绑定、提供者与作用域组织依赖。它可用于 Android,也可用于更广泛的 Kotlin 项目。若团队已有 Kodein 基础设施,继续维护通常合理;新项目要比较它与 Koin、Hilt 的文档、社区插件和长期维护成本,而不是只看 DSL 写起来是否顺手。
Toothpick 常见于较早的 Android 项目,强调作用域树,并通过注入机制把对象装配到页面或组件。阅读存量代码时,要先找 scope 的打开、关闭与父子关系。新增项目通常优先评估维护活跃度和 Jetpack 集成成本;它不是今天 Android 官方推荐的默认方案。
还有一些框架或插件会出现在 DI 讨论里,但角色不同:
AndroidX Startup用来组织初始化顺序,不负责完整的依赖图与作用域;ViewModelProvider.Factory负责创建 ViewModel,可以与任何 DI 方案配合;- Dagger 的构建辅助插件并不是替代 Hilt/Dagger 的完整 DI 容器;
- 在
Application放一个全局对象表属于手动管理或 Service Locator,是否满足 DI 的设计目标取决于依赖是否显式传入。
八、现在主流的是哪个?
如果问题是"新的原生 Android + Jetpack 项目,先学哪个、先选哪个 ",答案通常是 Hilt。截至本文写作的 2026 年 10 月,Android 官方依赖注入文档推荐使用 Hilt;它与 Android 生命周期和常见 Jetpack 组件衔接自然,背后仍是 Dagger 的编译期依赖图。
但"主流"不是一个可以脱离场景的市场份额排名:
- Hilt:官方推荐路径,原生 Android 团队常用的默认选择;
- Koin:Kotlin 社区中常见的轻量方案,尤其在追求 DSL 简洁或 KMP 共享时有吸引力;
- Dagger:大量成熟项目的底层基础,也是在 Hilt 不适合时的精细控制方案;
- 手动 DI:小规模项目仍完全合理。
不应因为 Hilt 是默认选项就迁移一套稳定的 Koin/Dagger 项目。迁移应由具体问题驱动,例如运行时漏绑频繁、测试替换困难、构建时间不可接受,或者团队跨平台策略变化。
九、按项目场景选型
| 场景 | 优先考虑 | 原因 |
|---|---|---|
| 新的 Android + Compose + Jetpack 项目 | Hilt | 生命周期和官方文档配套,依赖图编译期校验 |
| 少量页面、依赖很简单 | 手动 DI | 不引入额外构建与学习成本 |
| Kotlin Multiplatform 共享业务层 | Koin 或手动 DI | 业务绑定较容易跨端共享;平台入口仍需各自处理 |
| 已有大型 Dagger 图 | 继续 Dagger,按需评估 Hilt | 避免只为统一风格进行高成本迁移 |
| 迭代快、团队熟悉 Kotlin DSL | Koin | 定义与修改依赖较直接,需补足图校验测试 |
| 特殊组件或动态作用域要求很强 | Dagger 或定制容器 | 可以精细控制组件边界与生命周期 |
| 维护 Kodein/Toothpick 老项目 | 先沿用并评估风险 | 优先确保现有图、测试与升级路径可控 |
选型时最好做一个小样本:让 ViewModel → Repository → API/DAO 跑通,再加入两个同类型客户端、一个页面作用域、一个测试替身。这样比只比较"注解多不多"更能看出真实成本。
十、实践中最常见的坑
1. 把 DI 当成"消灭构造函数"
DI 最清晰的入口恰恰是构造函数。类需要什么就显式写在构造函数里;字段注入主要留给框架创建、自己无法控制构造过程的 Android 类。若业务类到处通过全局容器 get(),依赖会被藏起来,测试也更难。
2. 把所有东西都设为单例
网络客户端、数据库这类资源通常适合应用级复用;页面状态、一次请求的临时对象不一定适合。作用域应跟对象真正需要的生命周期一致。长生命周期对象持有短生命周期对象,是 Android 内存泄漏的常见来源。
3. 误以为 DI 等于"自动管理线程"
DI 只负责组装和提供对象。Repository 的网络调用、协程调度、错误处理与取消仍需要单独设计。注入了 CoroutineDispatcher 可以让测试更方便,但框架不会替你决定哪段代码应该在后台执行。
4. 多实现没有限定符
同样是 OkHttpClient,登录服务和上传服务可能需要不同拦截器。Hilt/Dagger 可用 @Qualifier 区分,Koin 可用命名限定符。若绑定含糊,轻则构建或启动失败,重则用到错误的客户端。
5. 测试只验证"能启动"
应测试关键依赖是否按预期替换、Scope 是否复用、页面销毁后是否释放。Hilt 可在测试中替换模块或绑定;Koin 可加载测试模块;手动 DI 可直接传入 fake。DI 让测试替换更方便,但不自动保证测试覆盖真实行为。
6. 多模块边界与 DI 图边界混为一谈
Gradle 模块是编译和依赖管理边界,DI Component/Module 是对象装配边界。一个业务模块可以包含多个绑定,一个 DI 组件也可以汇集多个 Gradle 模块。决定依赖方向时仍要遵守架构边界,不能靠 DI 框架掩盖不合理的模块耦合。
十一、给新项目的一条落地路线
- 先把核心对象改成构造函数注入,Repository 面向接口;
- 如果依赖关系简单,用
Application或入口层手动组装即可; - 当 Android 入口和生命周期装配明显增多时,优先试 Hilt;
- 如果项目采用 KMP 或团队已有 Koin 经验,同时评估 Koin;
- 明确 Scope 与 Qualifier,为关键绑定加入测试;
- 测量构建时间、启动和测试维护成本,再决定是否扩大覆盖范围。
最终结论很简单:**DI 是对象关系的设计,库只是替你维护这张关系图的工具。**Android 原生新项目一般先看 Hilt;Koin 是重要且成熟的替代选择;Dagger 适合需要底层控制或已有大规模图的团队。先把依赖方向与生命周期设计清楚,再选框架,往往比先选框架更有效。