一、为什么需要LiveData:只用ViewModel的局限
在上一篇的计数器写法中,数据保存在了ViewModel里,但更新UI仍然是手动调用setText:
btnAdd.setOnClickListener(v -> {
viewModel.add();
// 手动刷新UI
tvCount.setText(String.valueOf(viewModel.count));
});
这种写法存在很多潜在的问题:
- 无法自动感知页面生命周期。如果网络请求/异步任务在ViewModel里执行,回调时Activity已经退到后台或已经销毁,此时强行更新UI会导致空指针 、内存泄漏。
- 数据驱动能力弱。数据和UI强耦合,不符合MVVM的“数据驱动视图”思想;数据变化无法自动分发到多个观察者。
LiveData的核心定位:它是一个可观察的数据持有类,并且具备生命周期感知能力。数据变化时,自动通知处于活跃状态的页面更新UI,页面销毁时自动解绑,彻底解决上述问题。
二、LiveData核心特性
- 可观察:数据发生变化时,自动通知所有观察者(Activity/Fragment)
- 生命周期感知:只给处于活跃状态(STARTED/RESUMED)的页面分发数据,页面销毁自动解除订阅,不会内存泄漏
- 数据自动恢复:配置变更(旋转屏幕)导致页面重建,会自动收到最新的一次数据,不用手动保存恢复
- UI与数据解耦:ViewModel只负责生产数据,Activity只负责观察并渲染,互不持有
- 线程安全友好:区分主线程/子线程更新数据的API
三、基础使用:改造计数器案例
我们把上一篇文章的计数器,改成ViewModel + LiveData的标准写法。
1. 在ViewModel中定义LiveData
核心规则:
- 对外暴露只读的LiveData,防止外部随意修改数据
- 内部使用可写的MutableLiveData,只允许ViewModel内部修改数据
- 数据的所有变更逻辑,都封装在ViewModel内部
public class CounterViewModel extends ViewModel {
//内部可写的MutableLiveData,private修饰,只在ViewModel内部修改
private final MutableLiveData<Integer> _count = new MutableLiveData<>(0);
//对外暴露只读的LiveData,供Activity观察
public LiveData<Integer> getCount() {
return _count;
}
//加法业务方法
public void add() {
//获取当前值
Integer current = _count.getValue();
if(current == null) {
current = 0;
}
_count.setValue(current + 1);
}
//重置方法
public void reset() {
_count.setValue(0);
}
}
2. 在Activity中观察LiveData
使用observe()方法订阅数据变化,传入LifecycleOwner(也就是Activity自身),这是LiveData能感知生命周期的关键。
public class MainActivity extends AppCompatActivity {
private TextView tvCount;
private CounterViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
EdgeToEdge.enable(this);
setContentView(R.layout.activity_main);
ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
return insets;
});
tvCount = findViewById(R.id.tv_count);
Button btnAdd = findViewById(R.id.btn_add);
Button btnReset = findViewById(R.id.btn_reset);
//获取ViewModel实例
//通过ViewModelProvider获取实例,绑定当前Activity
viewModel = new ViewModelProvider(this).get(CounterViewModel.class);
//观察count数据的变化
viewModel.getCount().observe(this, new Observer<Integer>() {
@Override
public void onChanged(Integer integer) {
//数据变化时,自动回调这里,直接更新UI
tvCount.setText(String.valueOf(integer));
}
});
//点击事件只触发业务逻辑,不手动刷新UI
btnAdd.setOnClickListener(v -> viewModel.add());
btnReset.setOnClickListener(v -> viewModel.reset());
}
}
3、使用效果
- 点击按钮→ViewModel里的_count数据变化→LiveData自动触发onChanged→UI自动更新
- 旋转屏幕→Activity重建→重新observe→立刻收到最新的count值→UI自动恢复,无需手动保存
- 按Home键回到后台→LiveData停止分发数据,避免后台更新UI导致异常
- 退出界面→LiveData自动解绑,不会内存泄漏
四、核心API详解
1. LiveData与MutableLiveData的区别
| 类 | 可读性 | 使用场景 |
| LiveData<T> | 只读,只能观察,不能外部修改 | 对外暴露,供View层订阅,保证数据流向唯一 |
| MutableLiveData<T> | 可读可写,继承自LiveData | ViewModel内部使用,负责生产和修改数据 |
最佳实践:ViewModel永远对外暴露LiveData,内部用MutableLiveData,遵循数据单向流动原则:View→ViewModel触发事件→ViewModel修改LiveData→LiveData通知View更新。
2. setValue()与postValue()的区别
- setValue(T value)
- 必须在主线程调用
- 立刻更新值,立刻通知活跃的观察者
- 多次调用,每次都会触发回调
- postValue(T value)
- 可以在子线程调用,内部会自动切到主线程
- 不是立即执行,而是在post到主线程消息队列
- 短时间内多次调用,只有最后一次的值会被分发,中间值会被丢弃
使用规则:
- 主线程修改数据→用setValue
- 子线程(网络请求回调、IO线程)修改数据→用postValue
- 高频更新场景慎用postValue,可能丢中间值
3. observe()与observeForever()
- observe(LifecycleOwner owner, Observer observer)
- 绑定生命周期,页面活跃才回调,销毁自动解绑
- observeForever(Observer observer)
- 不绑定生命周期,只要数据变就一直回调
- 必须手动调用removeObserver()解绑,否则会内存泄漏
- 非特殊场景严禁使用
五、生命周期感知的底层原理
LiveData通过Lifecycle框架,监听Activity/Fragment的生命周期状态,只在活跃态分发数据。销毁态自动移除观察者。
简单流程:
- 调用observe(this, observer)时,LiveData会把观察者和LifecycleOwner绑定
- 当页面生命周期变化(onStart、onResume、onPause、onDestroy),LiveData都能收到通知
- 状态变为START或RESUMED(活跃态):视为可接收数据,有新数据就回调
- 状态变为DESTROYED(页面销毁):自动移除观察者,不会泄露,不会空指针
- 页面从后台回到前台:如果期间有数据自动更新,回到前台时会自动收到最新值
六、粘性事件(数据倒灌)与解决方案
1. 什么是粘性事件
LiveData有一个默认特性:新观察者订阅时,会立刻收到LiveData里当前存储的最后一次数据。
- 优点:旋转屏幕重建页面,能立刻恢复最新数据
- 缺点:对于“一次性事件”(比如Toast、弹窗、页面跳转)时,会出现“倒灌”问题
举一个聊天室的简单例子:
- 聊天页ViewModel里有一个toastMsg的LiveData,用来作为消息是否发送成功的标志位,不成功则弹提示
- 发送消息失败,setValue("发送失败"),页面谈了一次Toast
- 旋转屏幕,页面重建,重新observe,又收到“发送失败”,再弹一次Toast,甚至切后台回来,也可能重复弹
这就是粘性事件/数据倒灌,是LiveData最著名的坑。
2. 解决方案
日常开发最常用的方法是:Event包装类 + 单次消费标记
public class Event<T> {
private final T content;
private boolean hasBeenHandled = false;
public Event(T content) {
this.content = content;
}
//获取内容,如果未被消费则返回,否则返回null
public T getContentIfNotHandled() {
if(hasBeenHandled) {
return null;
}
hasBeenHandled = true;
return content;
}
//强制获取当前内容,无论是否消费过
public T peekCotent() {
return content;
}
}
ViewModel中使用:
private final MutableLiveData<Event<String>> _toast = new MutableLiveData<>();
public LiveData<Event<String>> getToast() {
return _toast;
}
public void sendMessage() {
// 发送失败,触发一次性提示
_toast.setValue(new Event<>("发送失败"));
}
Activity中观察:
viewModel.getToast().observe(this, event -> {
String msg = event.getContentIfNotHandled();
if (msg != null) {
Toast.makeText(this, msg, Toast.LENGTH_SHORT).show();
}
});
这样同一条消息只会被消费一次,旋转屏幕也不会重复弹出。
七、数据转换:Transformations
当需要对LiveData的数据做转换后再交给UI,可以用Transformations.map和Transformations.switchMap,无需自己写中间层。
1. map:一对一数据转换
比如计数器数字,要转成“当前计数:X”的字符串再显示:
public class CounterViewModel extends ViewModel {
private final MutableLiveData<Integer> _count = new MutableLiveData<>(0);
// 将Int类型的count,转换为String类型的展示文本
public final LiveData<String> countText = Transformations.map(_count, count -> {
return "当前计数:" + count;
});
public void add() {
Integer current = _count.getValue();
if (current == null) current = 0;
_count.setValue(current + 1);
}
}
UI层直接观察countText即可,不用自己做字符串拼接。
2. switchMap:触发式切换数据源
常用于“根据一个参数,切换不同的数据源”,比如根据用户ID查询用户信息:
private final MutableLiveData<String> userId = new MutableLiveData<>();
// userId变化时,自动切换到对应数据源
public LiveData<User> user = Transformations.switchMap(userId, id -> {
return repository.getUserById(id); // 返回一个LiveData
});
public void setUserId(String id) {
userId.setValue(id);
}
他跟map的区别在哪呢?
-
map:“加工”。把A变成B(比如把Int变成String)。同步的,瞬间完成。 -
switchMap:“换源”。因为A变了,所以把整个数据源换成另一个LiveData。异步的,要去拿新数据。

444

被折叠的 条评论
为什么被折叠?



