12、Hilt依赖注入(下):Hilt与Navigation的集成、@EntryPoint在非Android类中使用、Hilt的测试支持、Hilt与Dagger的对比与迁移

好,咱们接着聊Hilt。上一章我们把Hilt的基础用法和核心注解都过了一遍。这一章,咱们要深入一些实战场景——说白了,就是那些你在真实项目中一定会遇到的“硬骨头”。

我个人习惯把Hilt的学习分成三个阶段:会用、用好、用活。上一章是“会用”,这一章就是“用好”和“用活”。咱们要聊的这几个话题,每一个都是我当年在项目里踩过坑、填过坑的地方。

本章核心脉络:Hilt与Navigation的深度集成 → 用@EntryPoint突破Android限制 → 测试支持 → 与Dagger的对比迁移

Hilt依赖注入(下)知识体系 Hilt 进阶实战 Navigation 集成 @EntryPoint 测试支持 Dagger 对比迁移 @HiltViewModel NavBackStackEntry 作用域管理 ContentProvider Service 自定义Application @HiltAndroidTest 测试替身绑定 FragmentScenario 注解简化 自动生成组件 迁移策略

12.1 Hilt与Navigation的集成

先说说Navigation集成。这个场景太常见了——你的App有多个页面,用Navigation组件管理跳转,每个页面都有自己的ViewModel。怎么让Hilt给这些ViewModel注入依赖?

嗯,这里要注意。在Hilt出现之前,我们得手动写ViewModelFactory,代码又臭又长。Hilt直接把这个过程简化了。

核心做法就两步:

  1. 给ViewModel加上@HiltViewModel注解
  2. 在构造函数上用@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()
    }
}

关键点@TestInstallInreplaces属性指定了要替换的生产模块。测试运行时,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处理器)

迁移策略我建议分三步走:

  1. 替换注解:把@Component@Subcomponent替换成Hilt的@HiltAndroidApp@AndroidEntryPoint。这一步最机械,但也最容易出错。
  2. 调整Module:Dagger的Module需要加上@InstallIn注解,指定作用域。原来Dagger里的@Singleton作用域要改成@InstallIn(SingletonComponent::class)
  3. 处理自定义Scope:如果你在Dagger里自定义了Scope,需要映射到Hilt的对应组件。比如自定义的@ActivityScope可以映射到@ActivityScoped

个人建议:迁移时不要一次性全改完。我建议按模块逐步迁移——先迁移一个不重要的Feature模块,跑通流程后再推广到其他模块。这样风险可控,出了问题也好回滚。

嗯,说到这,Hilt的核心内容基本就覆盖完了。从基础用法到进阶技巧,从测试支持到迁移策略,这一套下来,你应该能在实际项目中游刃有余了。

记住一点——依赖注入不是银弹。它解决的是对象创建和依赖管理的问题,但业务逻辑的复杂度不会因为用了Hilt就消失。工具只是工具,关键还是看你怎么用。


公众号:蓝海资料掘金营,微信deep3321