第1章 依赖注入与Hilt入门
说到Android开发里的依赖注入,很多人第一反应是「不就是写个构造方法传参吗?搞那么复杂干什么」。说实话,我早年也是这么想的。那时候手写工厂类、自己维护单例池,项目模块少还行,后来一拆成十几个模块……嗯,维护地狱来了。
依赖注入的核心就一句话:把「怎么创建对象」这件事交给容器,你只管说「我要什么」。Hilt就是Google官方给出的Android DI容器方案,基于Dagger,但把很多繁琐配置给自动化了。
为什么需要依赖注入?
举个例子。你有一个网络请求的类 ApiService,在多个ViewModel里都要用。手动做法:每个ViewModel里去new一个ApiService,或者搞个ServiceLocator单例。问题很多:
- 单元测试时想换Mock很难,得改代码
- 模块间耦合度高,改了ApiService构造函数,到处都得跟着改
- 对象生命周期难管理,容易内存泄漏
依赖注入通过「控制反转」解决了这些。我印象很深,之前在一个日活千万级的App里,因为手动管理依赖,一次重构差点把整个模块搞崩……从那以后我就老老实实上DI了。
Hilt vs Dagger:为什么选Hilt?
Dagger是前辈,功能强大但要写很多模板代码:Component、Subcomponent、Module注册,还得自己管理Application的初始化。Hilt在Dagger的基础上做了三件事:
- 自动生成Component(通过
@HiltAndroidApp绑定Application生命周期) - 提供Android专属入口(
@AndroidEntryPoint) - 内置了ViewModel、Fragment等常见组件的作用域支持
说白了,你用Dagger需要手写一堆配置的活儿,Hilt用注解帮你自动完成了。这还是编译期处理的——不会拖慢运行性能。
Hilt核心组件详解
Hilt的用法其实就几条注解,但理解背后的组件关系很重要。我们先拆开看:
| 注解 | 作用位置 | 说明 |
|---|---|---|
@HiltAndroidApp | Application类 | 生成Hilt的根组件,所有依赖注入的起点 |
@AndroidEntryPoint | Activity / Fragment / View / Service | 给Android组件添加注入入口 |
@Inject | 构造方法 / 字段 | 告诉Hilt「这个对象我需要你帮我创建/注入」 |
@Module + @Provides | 类 + 方法 | 提供第三方库或无法用@Inject构造的对象 |
@Binds | 抽象方法 | 将接口绑定到具体实现 |
@Singleton / @ActivityScoped | 类或方法 | 控制对象作用域,避免重复创建 |
来看一个实际的例子。一个典型的主页Activity需要注入Repository:
// 1. Application类
@HiltAndroidApp
class MyApplication : Application()
// 2. 主页Activity
@AndroidEntryPoint
class HomeActivity : AppCompatActivity() {
@Inject lateinit var repository: HomeRepository
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// repository 可以直接用啦
}
}
// 3. Repository的构造
class HomeRepository @Inject constructor(
private val api: ApiService
) {
// ...
}
// 4. 如果ApiService是第三方库,用@Module提供
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Provides
@Singleton
fun provideApiService(): ApiService {
return ApiService.Builder()
.baseUrl("https://api.example.com")
.build()
}
}
你看,代码量非常少。Hilt通过注解自动帮你完成了:在Application创建时生成根Component → 在HomeActivity进入时从Component中获取HomeRepository → 递归解析HomeRepository依赖的ApiService。
@Lazy或Provider<T>来延迟创建。我建议在启动链路上尽量用懒加载,避免冷启动时一次创建太多对象。
Hilt的组件层次与作用域
Hilt的组件和Android生命周期是绑定的。下面是核心层级:
- SingletonComponent – 与Application共存,存活于整个进程
- ActivityComponent – 与Activity共存,Activity销毁时自动释放
- FragmentComponent – 与Fragment共存,生命周期更短
- ViewComponent, ServiceComponent, ViewModelComponent – 分别对应特定组件
有了这个层级,我们可以给对象加上作用域注解,让单例级别的对象不重复创建、Activity级别的对象随Activity销毁回收。举个避坑例子:我遇到过有人把数据库对象放在ActivityComponent里,结果每次旋转屏幕都重建一次数据库连接,后来改成@Singleton就好了。
性能初探:为什么Hilt能优化?
依赖注入经常被诟病「会不会让App变慢」?答案是:Hilt和Dagger都是编译期处理,运行时几乎零反射。对比Spring等运行时注入,Hilt生成的代码在你编译时就确定好了:
- 没有反射调用 – 所有依赖图在编译期通过APT生成绑定代码
- 对象复用 – 通过作用域注解控制单例,避免重复创建
- 懒加载支持 –
@Lazy和Provider能延迟创建不常用的依赖 - 模块化分包 – 每个模块可以独立声明自己的Module,Hilt自动合并
@Singleton作用域,每次创建ViewModel都会新建一个数据库实例,内存直接飙升。后来加上@Singleton + @Lazy,内存和启动时间都降下来了。所以作用域不是摆设,一定要根据实际场景选择。
依赖注入的初衷就是让代码更清晰、更易测试。Hilt则把Dagger的灵活性封装成了Android开发者友好的API。上手很简单,但深入理解组件层次和作用域,才能真正发挥它的性能优势。