12、DataBinding与协程:在DataBinding中调用挂起函数、Flow与DataBinding结合、异步数据加载、协程作用域管理。
说实话,DataBinding 刚出来那会儿,大家觉得它就是个省掉 findViewById 的工具。后来发现它能绑定数据,再后来发现它还能跟 LiveData 配合。但真正让 DataBinding 变得「能打」的,是它和 Kotlin 协程的结合。
我最早在项目里尝试 DataBinding + 协程时,踩了不少坑。比如在 XML 里直接调用挂起函数,结果 UI 线程直接崩了。又比如 Flow 收集到一半,Activity 销毁了还在发数据……嗯,这些坑我今天都会讲到。
核心观点:DataBinding 本身是同步的,但协程是异步的。让两者协同工作的关键,在于「作用域管理」和「生命周期感知」。
12.1 在 DataBinding 中调用挂起函数
你可能会问:XML 里怎么调用挂起函数?
答案是——不能直接调。DataBinding 的表达式是在主线程执行的,而挂起函数需要在协程里跑。所以我们需要一个「中间人」。
我个人习惯的做法是:在 ViewModel 里暴露一个普通函数,内部启动协程,然后把结果通过 LiveData 或 ObservableField 暴露给 DataBinding。
// ViewModel 中
class MyViewModel : ViewModel() {
val userData = MutableLiveData<User>()
fun loadUser(userId: String) {
viewModelScope.launch {
val user = repository.fetchUser(userId) // 挂起函数
userData.postValue(user)
}
}
}
然后在 XML 里绑定这个普通函数:
<Button
android:onClick="@{() -> viewModel.loadUser(viewModel.userId)}"
android:text="加载用户" />
这样看起来很简单对吧?但我在项目中遇到过一个问题:如果用户疯狂点击按钮,会启动多个协程。解决办法是用一个 Job 来管理:
private var loadJob: Job? = null
fun loadUser(userId: String) {
loadJob?.cancel()
loadJob = viewModelScope.launch {
val user = repository.fetchUser(userId)
userData.postValue(user)
}
}
小技巧:如果你非要在 XML 里直接调用挂起函数,可以用 BindingAdapter 配合协程。但说实话,我觉得那样代码可读性会变差,不推荐。
12.2 Flow 与 DataBinding 结合
Flow 是协程的「亲儿子」,天生就是异步的。但 DataBinding 不认识 Flow,它只认识 LiveData 和 ObservableField。
怎么让 Flow 和 DataBinding 握手?
答案是:把 Flow 转成 LiveData。
class MyViewModel : ViewModel() {
private val _users = MutableStateFlow<List<User>>(emptyList())
val users: StateFlow<List<User>> = _users.asStateFlow()
// 暴露给 DataBinding 的 LiveData
val usersLiveData: LiveData<List<User>> = _users.asLiveData()
}
XML 里直接绑定 LiveData:
<TextView
android:text="@{viewModel.usersLiveData.size()}"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
你想想看,这样做的好处是什么?Flow 在后台做数据流处理,LiveData 在前端做 UI 绑定,各司其职。
我曾经在一个电商项目里用这种方式做商品列表的实时搜索。用户输入关键词,Flow 做 debounce 和过滤,然后通过 LiveData 通知 DataBinding 更新 UI。效果非常流畅。
注意:Flow 转 LiveData 时,默认的转换策略是「Latest」。也就是说,如果 Flow 发射数据的速度比 UI 消费快,中间的旧数据会被丢弃。这在大多数场景下是合理的,但如果你需要保留所有数据,记得用别的策略。
12.3 异步数据加载
异步加载数据,说白了就是「别让用户干等着」。DataBinding 本身不负责加载数据,它只负责展示。但我们可以通过 BindingAdapter 来封装异步加载的逻辑。
举个例子,加载网络图片:
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String?) {
if (url == null) return
// 在协程中加载图片
CoroutineScope(Dispatchers.IO).launch {
val bitmap = loadBitmapFromUrl(url)
withContext(Dispatchers.Main) {
view.setImageBitmap(bitmap)
}
}
}
XML 里直接写:
<ImageView
app:imageUrl="@{viewModel.imageUrl}"
android:layout_width="100dp"
android:layout_height="100dp" />
但这里有个大坑——协程作用域管理。上面这段代码里,我直接用了 CoroutineScope(Dispatchers.IO),这意味着协程的生命周期不受任何控制。如果 Activity 销毁了,协程还在跑,就会出问题。
正确的做法是:把协程作用域绑定到 View 的生命周期上。
@BindingAdapter("imageUrl")
fun loadImage(view: ImageView, url: String?) {
if (url == null) return
// 使用 View 的协程作用域
view.doOnAttach {
lifecycleScope.launch {
val bitmap = withContext(Dispatchers.IO) {
loadBitmapFromUrl(url)
}
view.setImageBitmap(bitmap)
}
}
}
关键点:异步加载的核心不是「怎么加载」,而是「什么时候取消」。不管理好协程作用域,内存泄漏和崩溃就是家常便饭。
12.4 协程作用域管理
说到作用域管理,我见过太多人在这里翻车了。说白了,协程作用域就是协程的「生命周期容器」。容器销毁了,里面的协程也得跟着销毁。
在 DataBinding 的上下文中,我们主要关注三个作用域:
| 作用域 | 生命周期 | 适用场景 |
|---|---|---|
| viewModelScope | 与 ViewModel 共存亡 | 数据加载、业务逻辑 |
| lifecycleScope | 与 Activity/Fragment 共存亡 | UI 相关的异步操作 |
| 自定义 CoroutineScope | 由开发者控制 | 特殊场景(如定时器) |
我建议的实践原则是:
- ViewModel 里的协程用 viewModelScope
- BindingAdapter 里的协程用 lifecycleScope(通过 View 获取)
- 尽量不要在 DataBinding 表达式里启动协程
我曾经在一个项目里,因为偷懒在 BindingAdapter 里用了 GlobalScope,结果 Activity 销毁后协程还在跑,导致了一个极其隐蔽的内存泄漏。排查了两天才找到原因……从那以后,我对作用域管理就特别敏感。
避坑指南:如果你在 Fragment 里使用 DataBinding,记得在 onDestroyView 里取消那些与 View 相关的协程。因为 Fragment 的 View 可能被销毁,但 Fragment 本身还活着。
最后总结一下。DataBinding 和协程的组合,说白了就是「同步的壳,异步的核」。XML 里写绑定表达式,背后是协程在做异步操作。而这一切能跑得稳,全靠作用域管理得当。
我个人觉得,掌握了这四块内容——挂起函数调用、Flow 转换、异步加载、作用域管理——你基本就能在项目里游刃有余了。剩下的就是多写多练,踩坑多了自然就熟了。