为什么需要依赖注入
Activity 里 new Retrofit、new Repository、new Database——一行 new 把三层焊死,换实现要改源码,写测试还要绕开真实网络。依赖注入的目标就是:谁需要对象,谁声明要什么,由外部容器负责造好递进来。
依赖注入的本质:把"创建依赖"和"使用依赖"分开。
这篇用 Hilt 把这套机制在安卓落地。
DI 的核心思想
面向接口编程:ViewModel 依赖一个 Repository 接口,具体是网络仓库还是本地仓库,由容器决定。这样上层不关心实现细节,更换实现对上层透明。Hilt 是安卓官方的 DI 框架,把这套自动装配做进编译期。
class NewsViewModel @Inject constructor(
private val repo: NewsRepository
) : ViewModel()
构造函数加 @Inject,Hilt 自动把 NewsRepository 造好传进来。
接入 Hilt
第一步:Application 加 @HiltAndroidApp。第二步:需要注入的 Activity/Fragment 加 @AndroidEntryPoint。之后该组件里就能用 @Inject 拿对象。编译时 Hilt 会生成完整的依赖图。
- @Inject声明需要注入的依赖。
- @Module集中提供自己造不了的对象。
- @Singleton全局单例。
- @HiltViewModelViewModel 注入。
@Inject 与模块提供
自己写的类,构造函数加 @Inject 即可。三方类(Retrofit、OkHttp)没法改它们源码,就在 @Module 里用 @Provides 写出如何构造。模块把这些"造不出来的对象"登记进容器。
作用域与生命周期
不带作用域的对象每次都新建;@Singleton 全局一份;@ActivityScoped 在 Activity 存活期间复用。Retrofit 客户端应是单例(重),而每个 ViewModel 各拿各的实例。作用域决定对象活多久。
配合分层架构
UI 层(ViewModel)→ 领域/仓库层 → 数据层(网络/本地)。每一层只依赖下一层的抽象接口,Hilt 负责把具体实现注入进来。分层加上 DI,改动被限制在局部——换个数据源,只改模块里的提供方法。
替换实现便于测试
这是 DI 最大的收益:单测里给 ViewModel 注入一个假的 NewsRepository,返回写死的数据,不碰网络。生产环境注入真仓库。代码不用为测试改一行,靠容器换实现。
- 接口隔离让 mock 易写。
- 模块替换让测试可注入假实现。
- 构造注入让依赖一目了然。
实战:注入一个仓库
定义接口,模块提供实现,ViewModel 直接用。
interface NewsRepository
class RemoteNewsRepository @Inject constructor() : NewsRepository
@Module @InstallIn(SingletonComponent::class)
abstract class RepoModule {
@Binds abstract fun bindRepo(impl: RemoteNewsRepository): NewsRepository
}
@HiltViewModel
class NewsVM @Inject constructor(val repo: NewsRepository) : ViewModel()
整条链就这样:谁要什么,容器就从对应模块取来递上。依赖注入把装配的复杂度收进编译期,业务代码只表达"我需要什么",架构分层因此真正可维护、可替换、可测试。