第8章:Hilt与Fragment——Fragment中依赖注入的注意事项、Fragment与Activity共享依赖
Fragment,说实话,是Android依赖注入里最容易踩坑的地方。
为什么?因为Fragment的生命周期太复杂了。它不像Activity那样简单直接,Fragment有onAttach、onCreate、onCreateView、onViewCreated……这一串回调,而且还能被销毁重建。我早期用Dagger的时候,就在Fragment上翻过车——注入的依赖在Fragment重建后变成了null,查了半天才发现是注入时机不对。
这一章,我们就来聊聊Fragment里用Hilt的那些门道。
8.1 Fragment的注入时机
先看一个最基础的例子。用Hilt给Fragment注入依赖,你只需要加一个注解:
@AndroidEntryPoint
class UserProfileFragment : Fragment() {
@Inject
lateinit var userRepository: UserRepository
override fun onAttach(context: Context) {
super.onAttach(context)
// 注意:此时userRepository还没有注入!
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 这里userRepository已经可用了
Log.d("TAG", "userRepository = $userRepository")
}
}
嗯,这里要注意:@Inject标注的字段,在super.onCreate()之后才真正注入完毕。如果你在onAttach里就急着用,那肯定拿到的是null。
我曾经在onAttach里直接调用注入的依赖,结果空指针异常。后来养成习惯:所有依赖都在onCreate之后使用,或者在onViewCreated里初始化UI。
8.2 Fragment与Activity共享依赖
实际项目中,Fragment经常需要访问Activity级别的依赖。比如,一个登录Activity里有多个Fragment,它们都需要共享同一个SessionManager。
Hilt提供了很优雅的方式——@ActivityScoped。
@Module
@InstallIn(ActivityComponent::class)
object AppModule {
@Provides
@ActivityScoped
fun provideSessionManager(): SessionManager {
return SessionManager()
}
}
这样,Activity和它里面的所有Fragment,拿到的都是同一个SessionManager实例。
在Fragment里获取Activity级别的依赖,有两种方式:
- 直接注入——如果依赖是
@ActivityScoped的,Fragment里直接@Inject就行。 - 通过Activity获取——用
(requireActivity() as MainActivity).sessionManager,但我不推荐这么做,耦合太强。
我个人倾向于用第一种方式。只要把依赖声明为@ActivityScoped,Fragment里直接注入,代码干净,测试也方便。
8.3 FragmentScope vs ActivityScope
你可能会问:那Fragment自己的作用域呢?
Hilt提供了@FragmentScoped。它和@ActivityScoped的区别,说白了就是生命周期不同:
| 作用域 | 生命周期 | 适用场景 |
|---|---|---|
| @ActivityScoped | Activity创建到销毁 | 整个Activity共享的依赖,如SessionManager |
| @FragmentScoped | Fragment创建到销毁 | 仅当前Fragment需要的依赖,如列表适配器 |
| @Singleton | 应用全局 | 全局唯一的依赖,如Retrofit实例 |
举个例子:
@Module
@InstallIn(FragmentComponent::class)
object FragmentModule {
@Provides
@FragmentScoped
fun provideUserAdapter(): UserAdapter {
return UserAdapter()
}
}
这样,每个Fragment都有自己的UserAdapter实例,互不干扰。
选择作用域时,问自己一个问题:这个依赖应该活多久?如果多个Fragment共享,用@ActivityScoped;如果每个Fragment独立,用@FragmentScoped。
8.4 避坑:Fragment重建与注入
Fragment有个很坑的地方——它会被系统重建。比如屏幕旋转时,Fragment会走onDestroy然后重新创建。这时候,Hilt会重新注入依赖。
但如果你在Fragment里保存了状态,比如在onSaveInstanceState里存了数据,重建后注入的依赖是新的实例,之前的状态就丢了。
怎么解决?
- 用
ViewModel保存状态,ViewModel本身是@HiltViewModel,可以注入依赖。 - 或者,把需要持久化的数据存到
SavedStateHandle里。
@HiltViewModel
class UserViewModel @Inject constructor(
private val userRepository: UserRepository,
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val userId: String
get() = savedStateHandle.get("userId") ?: ""
}
这样,即使Fragment重建,ViewModel里的数据还在。
有一次,我在Fragment里用@Inject注入了一个网络请求的依赖,然后在onSaveInstanceState里保存了请求结果。结果屏幕旋转后,Fragment重建,注入的是新的依赖实例,但保存的结果还在——这就导致新旧数据不一致。后来改用ViewModel,问题解决。
8.5 知识体系总览
下面这张图,帮你理清Fragment依赖注入的核心逻辑:
8.6 实战建议
最后,给你几个我在项目中总结出来的建议:
- 能用ViewModel就别直接注入Fragment——ViewModel的生命周期更可控,而且天然支持状态保存。
- Fragment之间共享数据,用ActivityScoped + ViewModel——别用EventBus或者全局变量,那会让依赖关系变得一团糟。
- 注入的依赖如果涉及UI,记得在onViewCreated里初始化——因为onCreate时视图还没创建,你拿不到View。
- 测试时,用@ActivityScoped的依赖更容易mock——因为作用域明确,替换起来方便。
如果你不确定某个依赖应该用@ActivityScoped还是@FragmentScoped,先默认用@ActivityScoped。等发现多个Fragment需要独立实例时,再改成@FragmentScoped。这样改动成本最低。
好了,这一章的内容就到这里。Fragment的依赖注入,说白了就是把握好时机和范围。时机对了,依赖不会空;范围对了,数据不会乱。
公众号:蓝海资料掘金营,微信deep3321