Android 依赖注入库到底应该怎么选

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 框架掩盖不合理的模块耦合。

十一、给新项目的一条落地路线

  1. 先把核心对象改成构造函数注入,Repository 面向接口;
  2. 如果依赖关系简单,用 Application 或入口层手动组装即可;
  3. 当 Android 入口和生命周期装配明显增多时,优先试 Hilt;
  4. 如果项目采用 KMP 或团队已有 Koin 经验,同时评估 Koin;
  5. 明确 Scope 与 Qualifier,为关键绑定加入测试;
  6. 测量构建时间、启动和测试维护成本,再决定是否扩大覆盖范围。

最终结论很简单:**DI 是对象关系的设计,库只是替你维护这张关系图的工具。**Android 原生新项目一般先看 Hilt;Koin 是重要且成熟的替代选择;Dagger 适合需要底层控制或已有大规模图的团队。先把依赖方向与生命周期设计清楚,再选框架,往往比先选框架更有效。

参考资料

相关推荐
大圣编蚕11 小时前
初探 NDK 的世界:从零开始理解 Android 原生开发
android
云贝贝贝11 小时前
MySQL 慢查询定位与索引优化实战
android
骑着蜗牛撵大象32712 小时前
服务下线不再炸:分层关闭、TCP 存活探测与负载均衡排水的落地套路
android·tcp/ip·负载均衡·tcp·高可用·健康检查·优雅关闭
三少爷的鞋14 小时前
别再这么写协程了!Marcin Moskała 剖析的 几个常见协程误区与重构
android
mmsx15 小时前
Android 防二次打包第一道锁:签名校验与完整性校验
android·kotlin
mmsx15 小时前
Android 混淆不等于安全:二次打包链路完整走一遍
android·kotlin
小宋102117 小时前
Agent轨迹级评测实战:工具选择、预算超限与回归门禁
android·网络·人工智能·回归
墨天梦19 小时前
D05_ViewModel与单向数据流
android·kotlin
蒸鱼Yuzheng20 小时前
Android 构建可复现性:APK 指纹、文件级差异与供应链审计
android·apk·devops·软件供应链·可复现构建