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>
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 框架,它只关心你传进来的对象对不对。
startKoin { }。我曾经忘了这一步,结果运行时直接崩溃,报错信息还不太明显,排查了好久。
16.3 ViewModel 的注入
ViewModel 的注入是 DI 和 DataBinding 结合的核心场景。为什么?因为 ViewModel 是连接 UI 和数据的桥梁,DataBinding 需要它来提供数据。
不管是 Hilt 还是 Koin,注入 ViewModel 的套路都差不多:
- 定义 ViewModel:把依赖通过构造函数注入进来。
- 注册 ViewModel:在 DI 容器里声明 ViewModel 的创建方式。
- 在 Fragment/Activity 里获取:用
by viewModels()或by viewModel()拿到实例。 - 传给 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>
onAttach() 之后,所以在 onCreateView() 里用是安全的。
16.5 知识体系总览
下面这张图展示了 DI 和 DataBinding 的整体协作关系:
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,因为它侵入性更小。