第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的基础上做了三件事:

  1. 自动生成Component(通过@HiltAndroidApp绑定Application生命周期)
  2. 提供Android专属入口(@AndroidEntryPoint)
  3. 内置了ViewModel、Fragment等常见组件的作用域支持

说白了,你用Dagger需要手写一堆配置的活儿,Hilt用注解帮你自动完成了。这还是编译期处理的——不会拖慢运行性能。

我的习惯: 如果是新项目或者正在做模块化重构,直接上Hilt。老项目迁移成本也不高,可以逐模块替换,Hilt和Dagger可以共存。

Hilt核心组件详解

Hilt的用法其实就几条注解,但理解背后的组件关系很重要。我们先拆开看:

注解作用位置说明
@HiltAndroidAppApplication类生成Hilt的根组件,所有依赖注入的起点
@AndroidEntryPointActivity / 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。

关键点: 所有注入都是在创建阶段完成的,但Hilt做了懒加载优化——对于没有立即需要的依赖,可以加上@Lazy或Provider<T>来延迟创建。我建议在启动链路上尽量用懒加载,避免冷启动时一次创建太多对象。

Hilt的组件层次与作用域

Hilt的组件和Android生命周期是绑定的。下面是核心层级:

  • SingletonComponent – 与Application共存,存活于整个进程
  • ActivityComponent – 与Activity共存,Activity销毁时自动释放
  • FragmentComponent – 与Fragment共存,生命周期更短
  • ViewComponent, ServiceComponent, ViewModelComponent – 分别对应特定组件

有了这个层级,我们可以给对象加上作用域注解,让单例级别的对象不重复创建、Activity级别的对象随Activity销毁回收。举个避坑例子:我遇到过有人把数据库对象放在ActivityComponent里,结果每次旋转屏幕都重建一次数据库连接,后来改成@Singleton就好了。

Hilt依赖注入架构全景 @HiltAndroidApp Application(SingletonComponent) @AndroidEntryPoint Activity(ActivityComponent) @AndroidEntryPoint Fragment(FragmentComponent) @HiltViewModel ViewModel(ViewModelComponent) @Inject Repository (来自Module或构造注入) @Inject ViewModel (懒加载) @Module + @Provides / @Binds 定义第三方依赖与接口绑定 性能优化关注点 • 编译期生成代码 • 作用域管控生命周期 • 懒加载减轻启动 • 模块化隔离依赖

性能初探:为什么Hilt能优化?

依赖注入经常被诟病「会不会让App变慢」?答案是:Hilt和Dagger都是编译期处理,运行时几乎零反射。对比Spring等运行时注入,Hilt生成的代码在你编译时就确定好了:

  • 没有反射调用 – 所有依赖图在编译期通过APT生成绑定代码
  • 对象复用 – 通过作用域注解控制单例,避免重复创建
  • 懒加载支持 – @Lazy 和 Provider 能延迟创建不常用的依赖
  • 模块化分包 – 每个模块可以独立声明自己的Module,Hilt自动合并
我曾经踩过的坑: 在ViewModel里注入一个重量级对象(比如Room数据库),如果没用@Singleton作用域,每次创建ViewModel都会新建一个数据库实例,内存直接飙升。后来加上@Singleton + @Lazy,内存和启动时间都降下来了。所以作用域不是摆设,一定要根据实际场景选择。

依赖注入的初衷就是让代码更清晰、更易测试。Hilt则把Dagger的灵活性封装成了Android开发者友好的API。上手很简单,但深入理解组件层次和作用域,才能真正发挥它的性能优势。