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 + 协程 调用挂起函数 ViewModel 中转 Flow 结合 转 LiveData 异步数据加载 BindingAdapter 协程作用域管理 viewModelScope / lifecycleScope 核心:生命周期感知 + 作用域绑定

最后总结一下。DataBinding 和协程的组合,说白了就是「同步的壳,异步的核」。XML 里写绑定表达式,背后是协程在做异步操作。而这一切能跑得稳,全靠作用域管理得当。

我个人觉得,掌握了这四块内容——挂起函数调用、Flow 转换、异步加载、作用域管理——你基本就能在项目里游刃有余了。剩下的就是多写多练,踩坑多了自然就熟了。

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