第20章:Koin与Fragment:fragmentScope、fragment注入的注意事项
Fragment,说实话,是Android依赖注入里最容易出幺蛾子的地方。
为什么?因为Fragment的生命周期太复杂了。它依附于Activity,又可能被销毁重建,还经常在FragmentManager里进进出出。我见过太多项目,Koin配置得挺好,一到Fragment就崩了。嗯,今天我们就来聊聊怎么搞定它。
20.1 FragmentScope:到底要不要用?
先说说scope。Koin提供了fragmentScope,说白了就是让某个实例的生命周期跟Fragment绑定。Fragment创建,它创建;Fragment销毁,它销毁。
我个人习惯是:能不scope就不scope。为什么?因为Fragment的scope管理起来很麻烦,尤其是嵌套Fragment、ViewPager+Fragment这种场景,scope的生命周期边界容易搞混。
但有些场景确实需要。比如一个Fragment里有个网络请求的ViewModel,你希望它只在当前Fragment存活期间存在。这时候scope就派上用场了。
核心原则:只有当你明确需要「Fragment级别的单例」时,才使用fragmentScope。否则,用single()或factory()就够了。
20.2 注入Fragment的正确姿势
Fragment的注入,其实就两种方式:
- 构造注入:通过构造函数传参。这是最推荐的方式。
- 属性注入:在Fragment的
onCreate或onViewCreated里调用inject()。
先看构造注入的例子:
class MyFragment(private val repository: MyRepository) : Fragment() {
// 直接使用repository,不需要inject()
}
然后在Koin模块里注册:
val myModule = module {
fragment { MyFragment(get()) }
}
这样写的好处是:依赖关系一目了然,测试也方便。我在项目中遇到过一个坑——用属性注入时,Fragment被重建后,属性还没注入就被调用了,直接空指针。换成构造注入后,再也没出过这个问题。
但构造注入有个限制:Fragment必须有无参构造函数才能被系统重建。你可能会问:「那构造注入后,系统重建怎么办?」
答案是:Koin会自动处理。它会在Fragment重建时重新注入依赖。前提是你得在Fragment的onCreate里调用inject()——嗯,这又绕回属性注入了。
我的建议:如果Fragment不需要被系统重建(比如你控制了返回栈),用构造注入。如果需要重建,用属性注入,但一定要在onCreate里调用inject(),别放在onViewCreated里。
20.3 避坑指南:我曾经踩过的坑
讲几个我真实遇到过的坑,你大概率也会碰到。
坑1:Fragment重建后,scope里的实例还是旧的
我曾经在一个项目里用fragmentScope管理一个网络请求的ViewModel。Fragment被销毁重建后,scope里的ViewModel还是旧的,导致数据错乱。
原因是什么?因为fragmentScope的生命周期跟Fragment的onDestroy绑定。如果Fragment只是被重建(比如旋转屏幕),onDestroy不会调用,scope就不会被清除。
解决方案:在onDestroyView里手动关闭scope,或者在onCreate里重新创建scope。
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// 每次创建时重新获取scope
val scope = getKoin().createScope("myScope", named("fragment"))
// 注入依赖
inject()
}
坑2:嵌套Fragment的scope冲突
父Fragment和子Fragment都用fragmentScope,结果scope的ID冲突了。Koin默认用Fragment的类名作为scope的ID,如果两个Fragment类名一样(比如都是ListFragment),就会出问题。
解决办法:给scope指定唯一的ID。
val scope = getKoin().createScope(
"fragment_${this.javaClass.simpleName}_${this.id}",
named("fragment")
)
坑3:在DialogFragment里注入
DialogFragment的生命周期跟普通Fragment不太一样。它的onCreate可能在show()之后才调用。如果你在onCreate里注入,可能拿不到正确的scope。
我建议:DialogFragment里直接用factory()注入,别用scope。省心。
20.4 知识体系图
下面这张图总结了Fragment注入的核心逻辑和注意事项:
20.5 最佳实践总结
说了这么多,最后给你一个可以直接用的清单:
| 场景 | 推荐方式 | 注意事项 |
|---|---|---|
| 普通Fragment(不重建) | 构造注入 | 确保Koin模块里用fragment { }注册 |
| 普通Fragment(可能重建) | 属性注入(onCreate中) | 配合inject(),不要用by inject() |
| 需要Fragment级别单例 | fragmentScope | 手动管理scope的创建和销毁 |
| 嵌套Fragment | 构造注入 + 唯一scope ID | 避免scope ID冲突 |
| DialogFragment | factory()注入 | 不要用scope,生命周期特殊 |
最后提醒一句:Fragment的注入,核心就是搞清楚「谁创建、谁销毁」。别把scope的生命周期搞混了,否则你会看到各种奇怪的NPE和内存泄漏。我当年在一个项目里排查了三天,最后发现是scope没关——嗯,从那以后我对scope就特别谨慎。