16、DataBinding与依赖注入:Hilt与DataBinding、Koin与DataBinding、ViewModel的注入、Fragment的注入

依赖注入(DI)和 DataBinding 放在一起讲,是因为它们解决的是同一个问题——代码的耦合。DataBinding 帮你解耦了视图和逻辑,DI 帮你解耦了对象的创建和使用。两者结合,你的代码会变得非常干净。

我个人习惯在项目里同时用这两样东西。DataBinding 负责 UI 绑定,Hilt 或 Koin 负责提供依赖。嗯,这里要注意:注入的对象必须能在 DataBinding 表达式中直接访问,否则绑定就没意义了。

16.1 Hilt 与 DataBinding

Hilt 是 Google 官方推荐的 DI 框架,基于 Dagger。它和 DataBinding 配合起来很自然。你只需要把 ViewModel 或者 UseCase 通过 Hilt 注入到 Fragment 或 Activity 里,然后丢给 DataBinding 就行。

先看一个简单的例子。假设我们有一个 UserViewModel,它需要从仓库拿数据:

@HiltViewModel
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {
    val userName = MutableLiveData("加载中...")

    fun loadUser() {
        viewModelScope.launch {
            userName.value = userRepository.getUserName()
        }
    }
}

然后在 Fragment 里注入这个 ViewModel:

@AndroidEntryPoint
class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModels()

    private var _binding: FragmentUserBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentUserBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewModel = viewModel
        return binding.root
    }

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        viewModel.loadUser()
    }

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

布局文件里直接绑定 ViewModel:

<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable name="viewModel" type="com.example.app.UserViewModel" />
    </data>
    <TextView
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:text="@{viewModel.userName}" />
</layout>
小技巧: 记得在 Fragment 里设置 binding.lifecycleOwner,否则 LiveData 的变化不会自动更新 UI。我刚开始用的时候漏了这一行,折腾了半天才发现。

16.2 Koin 与 DataBinding

Koin 是轻量级的 DI 框架,用 Kotlin DSL 写起来很舒服。如果你不喜欢 Dagger 的编译期注解,Koin 是个好选择。

Koin 和 DataBinding 配合的关键点在于:ViewModel 的创建方式不同。Koin 没有 @HiltViewModel 这样的注解,你需要用 viewModel { } 来注册。

先定义模块:

val appModule = module {
    single { UserRepository() }
    viewModel { UserViewModel(get()) }
}

然后在 Fragment 里注入:

class UserFragment : Fragment() {
    private val viewModel: UserViewModel by viewModel()

    private var _binding: FragmentUserBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(
        inflater: LayoutInflater, container: ViewGroup?,
        savedInstanceState: Bundle?
    ): View {
        _binding = FragmentUserBinding.inflate(inflater, container, false)
        binding.lifecycleOwner = viewLifecycleOwner
        binding.viewModel = viewModel
        return binding.root
    }
}

布局文件和 Hilt 的例子完全一样。说白了,DataBinding 不关心你用什么 DI 框架,它只关心你传进来的对象对不对。

注意: 用 Koin 时,记得在 Application 里调用 startKoin { }。我曾经忘了这一步,结果运行时直接崩溃,报错信息还不太明显,排查了好久。

16.3 ViewModel 的注入

ViewModel 的注入是 DI 和 DataBinding 结合的核心场景。为什么?因为 ViewModel 是连接 UI 和数据的桥梁,DataBinding 需要它来提供数据。

不管是 Hilt 还是 Koin,注入 ViewModel 的套路都差不多:

  1. 定义 ViewModel:把依赖通过构造函数注入进来。
  2. 注册 ViewModel:在 DI 容器里声明 ViewModel 的创建方式。
  3. 在 Fragment/Activity 里获取:用 by viewModels()by viewModel() 拿到实例。
  4. 传给 DataBinding:把 ViewModel 赋值给布局里的变量。

这里有个细节:ViewModel 的生命周期比 Fragment 长。即使 Fragment 重建了,ViewModel 还是同一个实例。所以 DataBinding 绑定 ViewModel 时,数据不会丢失。

我记得有一次,用户旋转屏幕后数据没了。排查后发现是 ViewModel 没注入成功,每次旋转都创建了新实例。后来改用 by viewModels() 才解决。

16.4 Fragment 的注入

Fragment 的注入稍微复杂一点。因为 Fragment 有自己的生命周期,而且经常被销毁重建。Hilt 用 @AndroidEntryPoint 注解,Koin 用 fragment { } 模块。

Hilt 的方式:

@AndroidEntryPoint
class ProfileFragment : Fragment() {
    @Inject lateinit var analytics: AnalyticsService

    private var _binding: FragmentProfileBinding? = null
    private val binding get() = _binding!!

    override fun onCreateView(...): View {
        _binding = FragmentProfileBinding.inflate(inflater, container, false)
        binding.analytics = analytics
        return binding.root
    }
}

Koin 的方式:

val fragmentModule = module {
    fragment { ProfileFragment() }
}

class ProfileFragment : Fragment() {
    private val analytics: AnalyticsService by inject()

    // 其余代码同上
}

布局文件里绑定:

<layout>
    <data>
        <variable name="analytics" type="com.example.app.AnalyticsService" />
    </data>
    <TextView
        android:text="@{analytics.trackId}" />
</layout>
核心要点: Fragment 注入后,它的依赖(比如 AnalyticsService)可以在 DataBinding 表达式中直接使用。但要注意,Fragment 的依赖注入发生在 onAttach() 之后,所以在 onCreateView() 里用是安全的。

16.5 知识体系总览

下面这张图展示了 DI 和 DataBinding 的整体协作关系:

DI + DataBinding 协作架构 DI 容器 Hilt / Koin ViewModel 工厂 Repository 单例 其他依赖 Fragment @AndroidEntryPoint by viewModels() @Inject lateinit binding.viewModel = vm DataBinding 布局文件 @{viewModel.userName} @{analytics.trackId} lifecycleOwner 依赖注入提供对象 → Fragment 中转 → DataBinding 自动更新 UI

16.6 避坑指南

最后分享几个我踩过的坑:

  • 生命周期不一致:DataBinding 绑定了 ViewModel,但 Fragment 销毁时没清理 binding,导致内存泄漏。记得在 onDestroyView() 里置空 _binding
  • Koin 的 ViewModel 注入:用 by viewModel() 时,如果模块里没注册,会抛异常。我曾经把模块写错了位置,结果一直报 No definition found
  • Hilt 的 Fragment 注入:父 Fragment 和子 Fragment 都要加 @AndroidEntryPoint,否则子 Fragment 的注入会失败。这个坑我印象很深,排查了整整一个下午。
  • DataBinding 表达式里调用方法:如果 ViewModel 里的方法需要参数,确保参数也是可观察的,否则 UI 不会更新。

嗯,总的来说,DI 和 DataBinding 是绝配。一个负责提供数据,一个负责展示数据。你只要把两者正确连接起来,代码会变得非常简洁。我个人建议新项目直接上 Hilt + DataBinding,老项目迁移可以考虑 Koin,因为它侵入性更小。


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