什么是 Hilt Module?如何自定义一个提供 Repository 实例的 Module?
解读
国内大厂面试中,这道题出现的频率仅次于“Hilt 与 Dagger 的区别”。面试官真正想确认的是:
- 你是否理解 Hilt 的“约定大于配置”思想——Module 只是 Dagger 的语法糖,但 Hilt 通过 @InstallIn 把组件生命周期管起来;
- 你能否在实战中写出“可测试、可替换、作用域正确”的 Repository 提供逻辑,而不是只会 @Inject constructor;
- 对作用域、线程、Context 来源等细节是否有坑位意识,比如 AndroidStudio 默认模板生成的 Module 经常把 @Singleton 误用在可配置对象上,导致热修复或单元测试时无法替换。
因此,回答时要先给出 Hilt Module 的准确定义,再给出一段可直接拷贝到项目里跑的代码,并主动把“为什么用 @Singleton”“为什么传 ApplicationContext”这两个高频追问点前置解释掉。
知识点
- Hilt Module:被 @Module 注解的普通类,配合 @InstallIn 指定 Android 组件(SingletonComponent/ActivityComponent 等),告诉 Hilt 如何提供无法用构造函数注入的实例。
- @Provides:Module 内方法级注解,显式声明实例化逻辑;返回值类型即依赖类型。
- @Singleton / @ViewModelScoped:作用域注解,必须和 @InstallIn 的组件一一对应,否则编译期报错。
- 限定符:当同一类型存在多实现时,用自定义限定符(如 @Named、@Qualifier)消除歧义。
- 组件层级:SingletonComponent → ViewModelComponent → ActivityComponent → FragmentComponent,生命周期依次缩短,禁止向上依赖。
- 静态生成规则:Hilt 在 kapt/apt 阶段生成 HiltModules_xxx 类,最终并入 Dagger 图;因此 Module 必须保证无参构造且非 final。
- 国内场景:国内项目往往同时存在 Google 官方组件与厂商定制 SDK,Repository 的依赖树里既有 RoomDatabase 又有第三方 AAR 中的 Context,需要显式提供 ApplicationContext 防止内存泄漏。
答案
Hilt Module 是“告诉 Hilt 如何创建那些不能自动构造的实例”的载体;通过 @Module + @InstallIn 把实例创建逻辑挂载到 Android 生命周期组件上,从而与 Hilt 的自动注入图打通。
下面给出国内项目中最常见场景:Repository 依赖了 Room 的 DAO 与 Retrofit 的 Service,且需要全局单例。
步骤 1:声明 Repository 接口与实现
interface UserRepository { suspend fun login(mobile: String): User }
class UserRepositoryImpl @Inject constructor(
private val api: UserApi,
private val dao: UserDao
) : UserRepository {
override suspend fun login(mobile: String): User {
val net = api.login(mobile)
dao.insert(net)
return net
}
}
步骤 2:自定义 Module
@Module
@InstallIn(SingletonComponent::class) // 跟随 Application 生命周期
object RepositoryModule {
@Provides
@Singleton
fun provideUserRepository(
api: UserApi,
dao: UserDao
): UserRepository = UserRepositoryImpl(api, dao)
}
步骤 3:在 ViewModel 中直接使用
@HiltViewModel
class LoginViewModel @Inject constructor(
private val repo: UserRepository
) : ViewModel() {
fun login(mobile: String) = viewModelScope.launch {
val user = repo.login(mobile)
// ...
}
}
要点解释
- 用 @Singleton 保证 Repository 在应用生命周期内只有一份,避免重复创建数据库连接。
- 不直接 @Inject constructor 到 UserRepositoryImpl,是因为接口与实现分离后,Hilt 无法对接口做动态代理;Module 负责把实现绑定到接口。
- 如果以后要做单元测试,只需在 androidTest 目录再写一个 @Module 并用 @TestInstallIn 替换掉正式实现即可,符合国内持续集成要求。
拓展思考
- 多实现冲突:当项目里同时存在 LocalUserRepository 和 RemoteUserRepository,可用自定义限定符
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Local
@Qualifier @Retention(AnnotationRetention.BINARY) annotation class Remote
在 Module 里分别 @Provides @Local 和 @Provides @Remote,调用方用 @Local UserRepository 注入即可,避免 Named 字符串写错。
-
作用域误用:Repository 如果持有 Activity 级 Context,却声明 @Singleton,会导致内存泄漏;正确姿势是只注入 Application,或用 @ActivityScoped + ActivityComponent。
-
替换策略:国内渠道包经常需要“华为账号/小米账号”两套 Repository 实现,可在 buildVariant 维度提供不同 Module,利用 Hilt 的 @TestInstallIn 原理做“正式替换”,比手写工厂优雅。
-
性能陷阱:Module 中 @Provides 方法若创建成本较高(如加密数据库),建议改成 @Provides @Singleton 并确保内部无耗时初始化;否则主线程触发注入会掉帧,面试时可主动提到“用 Dagger 的 Lazy 或 Kotlin 的 by lazy 延迟初始化”。
-
与 Jetpack Startup 结合:国内厂商对自启动管控严格,可在 Initializer 里只负责初始化数据库,Repository 仍由 Hilt 注入,避免双初始化带来的耗时与崩溃。