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 实现流程

  1. 创建抽象 BaseFragment:使用 abstract class BaseFragment<VB : ViewBinding>(layoutId: Int) 泛型化,传入布局 ID 和 ViewBinding 类型。
  2. 在基类上添加 @AndroidEntryPoint:确保所有子类自动获得 Hilt 注入能力。
  3. 定义抽象方法 initInject():在 onAttach 或 onCreate 中调用,子类实现具体的依赖注入逻辑。
  4. 子类继承 BaseFragment:只需提供布局和注入实现,不再添加任何 Hilt 注解。
步骤操作关键代码/配置
1创建 BaseFragment 类abstract class BaseFragment<VB: ViewBinding>(@LayoutRes layoutId: Int)
2添加 @AndroidEntryPoint@AndroidEntryPoint
abstract class BaseFragment<VB: ViewBinding>
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 与 BaseFragment 统一注入结构图 BaseFragment<VB> @AndroidEntryPoint @AndroidEntryPoint abstract onInject() HomeFragment @Inject ViewModel, Repository DashboardFragment @Inject ViewModel, Analytics SettingsFragment @Inject SharedPrefs, Logger 要点:BaseFragment 带 @AndroidEntryPoint,子类无需再写注解,但仍可在子类中使用 @Inject 声明具体依赖。

总结:通过 @AndroidEntryPoint 抽象基类,Hilt 的 Fragment 注入代码量减少 60% 以上,同时避免了重复注解带来的维护风险和编译错误。该模式已成为大型 Android 组件化项目的标准实践。