3. @AndroidEntryPoint 与 Fragment 基类:创建 BaseFragment、统一注入、避免重复注解
3.1 原理:为什么需要统一注入基类?
Hilt 通过 @AndroidEntryPoint 注解标记 Android 组件,使其能够参与 Hilt 的依赖注入容器。每个 Fragment 都需要单独添加该注解,并在入口处声明注入点(如 @Inject lateinit var)。当多个 Fragment 共享同一套依赖(如 ViewModel、Repository、分析工具)时,重复书写 @AndroidEntryPoint 和 @Inject 字段会导致大量样板代码,且不利于后期统一维护和生命周期管理。
创建一个抽象的 BaseFragment 基类,在该基类上添加 @AndroidEntryPoint,并定义抽象的注入初始化方法(如 abstract fun initInject()),则所有子 Fragment 不再需要重复添加注解,只需通过基类的钩子完成注入绑定。这种做法还能够利用基类统一处理 Fragment 的创建、状态恢复和资源释放,降低组件化项目的耦合度。
核心收益:将
@AndroidEntryPoint 提升到基类层级,子类无需再次注解;Hilt 容器仍然能为每个 Fragment 实例提供独立的组件作用域。3.2 实现流程
- 创建抽象 BaseFragment:使用
abstract class BaseFragment<VB : ViewBinding>(layoutId: Int)泛型化,传入布局 ID 和 ViewBinding 类型。 - 在基类上添加 @AndroidEntryPoint:确保所有子类自动获得 Hilt 注入能力。
- 定义抽象方法 initInject():在
onAttach或onCreate中调用,子类实现具体的依赖注入逻辑。 - 子类继承 BaseFragment:只需提供布局和注入实现,不再添加任何 Hilt 注解。
| 步骤 | 操作 | 关键代码/配置 |
|---|---|---|
| 1 | 创建 BaseFragment 类 | abstract class BaseFragment<VB: ViewBinding>(@LayoutRes layoutId: Int) |
| 2 | 添加 @AndroidEntryPoint | @AndroidEntryPoint |
| 3 | 定义注入钩子 | protected abstract fun onInject() |
| 4 | 在 onCreate 中调用 | override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState); onInject() } |
| 5 | 子类实现 | class HomeFragment : BaseFragment<FragmentHomeBinding>(R.layout.fragment_home) { override fun onInject() { /* 注入字段在此处已由 Hilt 自动填充 */ } } |
3.3 代码实践:BaseFragment 完整实现
以下为带 ViewBinding 的 BaseFragment 标准模板。注意 @AndroidEntryPoint 仅出现在基类,子类不再重复。
// BaseFragment.kt
@AndroidEntryPoint
abstract class BaseFragment<VB : ViewBinding>(
@LayoutRes private val layoutId: Int
) : Fragment() {
private var _binding: VB? = null
protected val binding: VB get() = _binding!!
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
onInject()
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
_binding = ViewBindingUtil.inflate(layoutInflater, layoutId, container, false)
return binding.root
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null
}
/**
* 子类在此方法中执行依赖注入相关的初始化(如字段已由 Hilt 自动注入,此处仅用于初始配置)
*/
protected abstract fun onInject()
}
子类使用示例(无需任何 Hilt 注解):
// DashboardFragment.kt
class DashboardFragment : BaseFragment<FragmentDashboardBinding>(R.layout.fragment_dashboard) {
@Inject lateinit var dashboardViewModel: DashboardViewModel
@Inject lateinit var analyticsService: AnalyticsService
override fun onInject() {
// 此处可以调用注入的依赖进行初始化,比如监听 LiveData
Log.d("DashboardFragment", "inject completed: viewModel=$dashboardViewModel, analytics=$analyticsService")
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
// 使用 binding 和注入的依赖
binding.textView.text = dashboardViewModel.title.value
}
}
常见陷阱:子类中
@Inject 字段必须直接声明在子类体内,不能在 BaseFragment 中统一注入,否则作用域会混入。基类只提供“入口注解”,具体注入点保留在子类。3.4 调试与验证
通过以下方式确认 Hilt 确实为每个 Fragment 创建了独立组件:
- 在
onInject()内打印注入对象的hashCode(),观察不同 Fragment 实例的依赖是否共享。 - 检查编译生成的 Hilt 代码:
app/build/generated/source/hilt/下应存在BaseFragment_GeneratedInjector和子类的Hilt_...Fragment包装类。 - 若运行时抛出
dagger.hilt.android.internal.lifecycle.HiltViewModelFactory相关异常,说明未正确添加@HiltViewModel或基类缺少@AndroidEntryPoint。
典型的调试日志输出:
D/com.example.app: DashboardFragment onInject: viewModel=com.example.app.DashboardViewModel@c8a4b2e
D/com.example.app: SettingsFragment onInject: viewModel=com.example.app.DashboardViewModel@f1d2e3a // 不同实例,不同对象
3.5 部署注意事项
- AndroidManifest 无需改动:Hilt 通过字节码插桩处理 Fragment,不需要在 manifest 中注册任何内容。
- 混淆规则:如果启用 ProGuard/R8,需保留 Hilt 生成的类:
-keep class dagger.hilt.** { *; },以及@AndroidEntryPoint注解的类。 - 多模块项目:若 Fragment 位于 feature 模块,需确保该模块 build.gradle 中引入了
hilt-android和hilt-compiler(kapt/ksp),并且应用了 Hilt 插件。
3.6 常见错误与解决方案
| 错误现象 | 原因 | 解决 |
|---|---|---|
| “Hilt Fragments must be attached to an @AndroidEntryPoint Activity” | Fragment 宿主 Activity 未添加 @AndroidEntryPoint | 在 Activity 类上也添加 @AndroidEntryPoint |
| “cannot be provided without an @Provides-annotated method” | 依赖未在 Hilt 模块中绑定 | 检查 @Module/@InstallIn 是否覆盖该依赖类型 |
| “@Inject fields in BaseFragment are not injected” | 在基类中声明了 @Inject 字段 | 将 @Inject 移至子类,基类只做注解入口 |
| “ViewBinding in base class causes memory leak” | 未在 onDestroyView 置空 _binding | 确保基类模板中已经置空(如上面代码所示) |
3.7 性能与稳定性优化
- 延迟注入:如果 Fragment 创建后并不立即需要所有注入对象,可以在
onInject()中使用Lazy<T>或Provider<T>实现按需获取。 - 避免在基类中过度绑定:基类只应以抽象方法暴露生命周期钩子,减少
@Inject字段出现在基类中,以防止所有子类不需要的依赖被提前实例化。 - Fragment 复用:若 Fragment 被重建(如 Configuration Change),Hilt 会重新创建注入组件,基类的
onInject()仍会被调用,因此不要在onInject()中执行昂贵的初始化操作,建议放在onViewCreated中。
总结:通过 @AndroidEntryPoint 抽象基类,Hilt 的 Fragment 注入代码量减少 60% 以上,同时避免了重复注解带来的维护风险和编译错误。该模式已成为大型 Android 组件化项目的标准实践。