12、Hilt依赖注入(下):Hilt与Navigation的集成、@EntryPoint在非Android类中使用、Hilt的测试支持、Hilt与Dagger的对比与迁移
好,咱们接着聊Hilt。上一章我们把Hilt的基础用法和核心注解都过了一遍。这一章,咱们要深入一些实战场景——说白了,就是那些你在真实项目中一定会遇到的“硬骨头”。
我个人习惯把Hilt的学习分成三个阶段:会用、用好、用活。上一章是“会用”,这一章就是“用好”和“用活”。咱们要聊的这几个话题,每一个都是我当年在项目里踩过坑、填过坑的地方。
本章核心脉络:Hilt与Navigation的深度集成 → 用@EntryPoint突破Android限制 → 测试支持 → 与Dagger的对比迁移
12.1 Hilt与Navigation的集成
先说说Navigation集成。这个场景太常见了——你的App有多个页面,用Navigation组件管理跳转,每个页面都有自己的ViewModel。怎么让Hilt给这些ViewModel注入依赖?
嗯,这里要注意。在Hilt出现之前,我们得手动写ViewModelFactory,代码又臭又长。Hilt直接把这个过程简化了。
核心做法就两步:
- 给ViewModel加上
@HiltViewModel注解 - 在构造函数上用
@Inject注解
@HiltViewModel
class UserProfileViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
// 业务逻辑...
}
然后你在Fragment里获取ViewModel时,直接用by viewModels()委托就行。Hilt会自动从NavBackStackEntry的作用域里找到对应的ViewModel。
@AndroidEntryPoint
class UserProfileFragment : Fragment() {
private val viewModel: UserProfileViewModel by viewModels()
// ...
}
个人经验:我刚开始用的时候,忘了给Fragment加@AndroidEntryPoint,结果ViewModel死活拿不到依赖。排查了半天才发现是这个问题。记住——Navigation集成的前提是Fragment和Activity都必须标注@AndroidEntryPoint。
还有一个细节——作用域管理。每个NavBackStackEntry都有自己的生命周期,Hilt会为每个NavBackStackEntry创建一个独立的组件作用域。这意味着,如果你在某个导航图里定义了@HiltViewModel,它的生命周期就和这个导航图绑定。回到上一级页面时,ViewModel会被自动清理。
为什么会这样?因为Hilt内部把NavBackStackEntry当成了一个组件持有者。每个Entry都有自己的组件树,互不干扰。
12.2 @EntryPoint:突破Android类的限制
接下来聊一个比较“硬核”的话题——@EntryPoint。
你想想看,Hilt的依赖注入默认只能在Android类里用——Activity、Fragment、Service、BroadcastReceiver这些。但现实项目中,总有一些“非Android类”也需要注入依赖。比如自定义的ContentProvider、JobService,或者某些第三方库的回调。
这时候@EntryPoint就派上用场了。
它的原理很简单:在Hilt的组件树上开一个“后门”,让非Android类也能拿到依赖。
@EntryPoint
@InstallIn(SingletonComponent::class)
interface MyEntryPoint {
fun provideAnalyticsService(): AnalyticsService
}
然后在非Android类里,通过EntryPointAccessors来获取:
class MyContentProvider : ContentProvider() {
override fun onCreate(): Boolean {
val entryPoint = EntryPointAccessors.fromApplication(
context,
MyEntryPoint::class.java
)
val analyticsService = entryPoint.provideAnalyticsService()
analyticsService.track("provider_init")
return true
}
}
避坑指南:我曾经在一个自定义Worker里用@EntryPoint,结果忘了指定@InstallIn的作用域。Worker是后台线程,但我的EntryPoint装在了SingletonComponent里,导致线程安全问题。后来我把作用域改成了@InstallIn(ServiceComponent::class)才解决。记住——EntryPoint的作用域要和实际使用场景匹配。
说白了,@EntryPoint就是Hilt留给你的“逃生通道”。当你遇到那些Hilt默认不支持的场景时,用它就对了。
12.3 Hilt的测试支持
测试这块,我多说几句。很多开发者觉得依赖注入和测试是两回事,其实不然。Hilt的设计初衷之一,就是让测试变得更简单。
Hilt为测试提供了专门的注解——@HiltAndroidTest。用它标注的测试类,会自动初始化Hilt的测试组件。
@HiltAndroidTest
@RunWith(AndroidJUnit4::class)
class UserProfileTest {
@get:Rule
var hiltRule = HiltAndroidRule(this)
@Inject
lateinit var userRepository: UserRepository
@Before
fun setup() {
hiltRule.inject()
}
@Test
fun testUserProfile() {
// 测试逻辑
}
}
这里有个关键点——hiltRule.inject()必须在@Before里调用。我见过有人直接在字段声明时用lateinit,然后忘了调用inject,结果跑测试时一直报NullPointerException。
测试替身(Test Double)的绑定也很方便。你可以在测试模块里用@Bind或@Provides来替换生产环境的依赖:
@Module
@TestInstallIn(
components = [SingletonComponent::class],
replaces = [ProductionModule::class]
)
object TestModule {
@Provides
fun provideUserRepository(): UserRepository {
return FakeUserRepository()
}
}
关键点:@TestInstallIn的replaces属性指定了要替换的生产模块。测试运行时,Hilt会自动用测试模块替换生产模块,不需要你手动切换。
还有一个实用技巧——用FragmentScenario测试Fragment时,Hilt会自动注入依赖。你只需要确保Fragment标注了@AndroidEntryPoint即可。
@Test
fun testUserProfileFragment() {
val scenario = launchFragmentInContainer<UserProfileFragment>()
scenario.onFragment { fragment ->
// fragment的依赖已经被Hilt注入好了
assertNotNull(fragment.viewModel)
}
}
12.4 Hilt与Dagger的对比与迁移
最后聊聊Hilt和Dagger的关系。很多老项目用的是Dagger,现在想迁移到Hilt。到底值不值得迁?怎么迁?
我直接说结论:如果你的项目从零开始,无脑选Hilt。如果项目已经在用Dagger,要看情况。
| 对比维度 | Dagger | Hilt |
|---|---|---|
| 学习成本 | 高,需要理解Component、Subcomponent、Scope等概念 | 低,注解更少,概念更简化 |
| 模板代码 | 多,需要手动创建Component和Builder | 少,自动生成Component |
| Android集成 | 需要手动处理生命周期 | 自动绑定Android生命周期 |
| 测试支持 | 需要手动配置测试组件 | 内置测试支持,开箱即用 |
| 编译速度 | 较快(但增量编译差) | 略慢(因为多了Hilt处理器) |
迁移策略我建议分三步走:
- 替换注解:把
@Component、@Subcomponent替换成Hilt的@HiltAndroidApp、@AndroidEntryPoint。这一步最机械,但也最容易出错。 - 调整Module:Dagger的Module需要加上
@InstallIn注解,指定作用域。原来Dagger里的@Singleton作用域要改成@InstallIn(SingletonComponent::class)。 - 处理自定义Scope:如果你在Dagger里自定义了Scope,需要映射到Hilt的对应组件。比如自定义的
@ActivityScope可以映射到@ActivityScoped。
个人建议:迁移时不要一次性全改完。我建议按模块逐步迁移——先迁移一个不重要的Feature模块,跑通流程后再推广到其他模块。这样风险可控,出了问题也好回滚。
嗯,说到这,Hilt的核心内容基本就覆盖完了。从基础用法到进阶技巧,从测试支持到迁移策略,这一套下来,你应该能在实际项目中游刃有余了。
记住一点——依赖注入不是银弹。它解决的是对象创建和依赖管理的问题,但业务逻辑的复杂度不会因为用了Hilt就消失。工具只是工具,关键还是看你怎么用。
公众号:蓝海资料掘金营,微信deep3321