4. MutableLiveData 与数据更新:让数据真正“活”起来

好,咱们接着聊 LiveData。上一节我们讲了 LiveData 是什么,说白了就是一个“数据观察者模式”的封装。但有个问题——LiveData 本身是只读的,你不能直接改它的值。那数据从哪来?答案就是它的子类:MutableLiveData

4.1 MutableLiveData 是什么?

MutableLiveData 是 LiveData 的一个直接子类。它暴露了两个关键方法:setValue()postValue()。这两个方法就是用来更新数据的。

我刚开始用的时候,觉得这玩意儿不就是多了一个“写”的能力吗?其实没那么简单。你想想看,如果 LiveData 是“只读”的,那 ViewModel 对外暴露的数据就应该用 LiveData,内部修改才用 MutableLiveData。这是一个很重要的设计原则。

核心原则:对外暴露 LiveData,对内使用 MutableLiveData。这样 UI 层只能观察,不能篡改数据源。

来看一个最简单的例子:

class CounterViewModel : ViewModel() {
    // 内部用 MutableLiveData
    private val _count = MutableLiveData<Int>(0)

    // 对外暴露不可变的 LiveData
    val count: LiveData<Int> get() = _count

    fun increment() {
        _count.value = (_count.value ?: 0) + 1
    }
}

注意这里我用了一个下划线前缀 _count。这是我个人的习惯——私有变量加下划线,公开的 LiveData 去掉下划线。这样一眼就能分清哪个是“可写的”,哪个是“只读的”。

4.2 setValue() 与 postValue() 的区别

好,重点来了。这两个方法都能更新数据,但使用场景完全不同。

方法 调用线程 行为特点 适用场景
setValue() 必须在主线程 立即更新,同步执行 UI 事件、按钮点击等主线程操作
postValue() 任意线程 异步更新,最终会切回主线程 网络请求、数据库查询等后台任务

为什么会这样?因为 LiveData 的观察者(也就是 UI 层)必须在主线程才能收到通知。如果你在子线程直接调用 setValue(),它会抛异常。我刚开始踩过这个坑——在协程的 IO 线程里调了 setValue(),结果 app 直接崩了。

注意:在子线程调用 setValue() 会抛出 IllegalStateException。记住,只有主线程才能直接更新 LiveData。

postValue() 呢?它会把更新任务 post 到主线程的消息队列里。说白了就是“你先忙你的,等主线程有空了再更新”。但这里有个坑——如果你连续多次调用 postValue(),只有最后一次的值会生效。因为 post 的任务会被合并。

我曾经在项目里遇到过一个问题:一个下载进度条,在子线程里每 100ms 调用一次 postValue(),结果 UI 上的进度跳来跳去,中间很多值都丢了。后来我改用 setValue() 配合 Handler 手动切线程,才解决了这个问题。

我的建议:如果你需要频繁更新数据(比如进度条),尽量在主线程用 setValue()。如果必须从子线程更新,用 postValue(),但要接受它可能会丢失中间值。

4.3 数据更新与 UI 自动刷新

LiveData 最爽的一点就是:你只管更新数据,UI 会自动刷新。不需要手动调用 notifyDataSetChanged() 或者 adapter.notify()。这就是“响应式”的魅力。

来看一个完整的例子:

class UserProfileViewModel : ViewModel() {
    private val _userName = MutableLiveData<String>()
    val userName: LiveData<String> get() = _userName

    private val _age = MutableLiveData<Int>()
    val age: LiveData<Int> get() = _age

    fun loadUser(userId: String) {
        // 模拟网络请求
        viewModelScope.launch {
            val user = api.fetchUser(userId)  // 子线程
            _userName.postValue(user.name)    // 用 postValue
            _age.postValue(user.age)
        }
    }

    fun updateName(newName: String) {
        _userName.value = newName  // 主线程,用 setValue
    }
}

在 Activity 或 Fragment 里,你只需要这样观察:

viewModel.userName.observe(this) { name ->
    textView.text = name
}

只要 _userName 的值变了,textView 就会自动更新。不需要写任何额外的刷新逻辑。这就是 LiveData 的核心价值——数据驱动 UI

4.4 避坑指南:我踩过的几个坑

嗯,这里我总结几个实际项目中容易踩的坑,希望能帮你少走弯路。

  • 坑一:在子线程用 setValue() —— 这个前面说过了,直接崩。记住,子线程用 postValue()
  • 坑二:连续 postValue 丢失中间值 —— 如果你在循环里调 postValue(),只有最后一个值会生效。解决方案:要么用 setValue() 切到主线程,要么用 LiveDataMediatorLiveData 做合并。
  • 坑三:忘记初始化默认值 —— 如果 MutableLiveData 没有初始值,第一次 observe 时不会触发回调。我习惯在声明时就给个默认值,比如 MutableLiveData("")MutableLiveData(0)
  • 坑四:暴露了 MutableLiveData —— 有些新手直接把 MutableLiveData 作为 public 属性暴露出去。这样外部可以直接调用 .value = xxx,破坏了封装性。记住,对外只暴露 LiveData。

4.5 知识体系图

下面这张图帮你理清 MutableLiveData 的核心逻辑:

MutableLiveData 数据更新核心逻辑 ViewModel _count: MutableLiveData<Int> 内部可写,外部不可见 count: LiveData<Int> 对外只读,UI 观察 setValue() postValue() UI 层(Activity/Fragment) observe(lifecycleOwner) { value -> } 自动刷新 UI 无需手动通知 数据变化自动推送 setValue() 必须在主线程 · postValue() 可在任意线程

4.6 小结

MutableLiveData 是 LiveData 的“写入口”。记住三点:

  • 内部用 MutableLiveData,对外暴露 LiveData
  • 主线程用 setValue(),子线程用 postValue()
  • 数据更新后,UI 自动刷新——这是 LiveData 最爽的地方

下一节我们会聊 MediatorLiveData,它能把多个 LiveData 合并成一个,非常实用。到时候我会分享一个我在项目中用它做“表单验证”的真实案例。


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