ViewModel + LiveData + Lifecycle
深入 ViewModel 作用域与生命周期、LiveData 概念与转换、MediatorLiveData 与 LifecycleOwner 观察机制,并以数据加载场景实战演示。
学习目标
- 理解 ViewModel 的作用域与生命周期绑定规则
- 掌握 LiveData 与 LifecycleOwner 的观察机制
- 能够使用 Transformations.map / switchMap 与 MediatorLiveData 组合数据
- 用 ViewModel + LiveData 完成一个数据加载示例
学习目标
- 理解 ViewModel 的作用域与生命周期绑定规则;
- 掌握 LiveData 与 LifecycleOwner 的观察机制;
- 能够使用
Transformations.map/switchMap与MediatorLiveData组合数据; - 用 ViewModel + LiveData 完成一个数据加载示例。
ViewModel + LiveData + Lifecycle
依赖配置
dependencies {
implementation "androidx.appcompat:appcompat:1.7.0"
implementation "androidx.core:core-ktx:1.13.1"
// Lifecycle / ViewModel / LiveData
implementation "androidx.lifecycle:lifecycle-viewmodel:2.8.4"
implementation "androidx.lifecycle:lifecycle-livedata:2.8.4"
implementation "androidx.lifecycle:lifecycle-runtime:2.8.4"
implementation "androidx.activity:activity:1.9.1"
// Java 8+
coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.0.4"
}
ViewModel 作用域与生命周期
ViewModel 的设计目标
ViewModel 的核心目标是 在配置变更(如旋转屏幕)时保留 UI 数据,避免重新加载与状态丢失。它的生命周期并不绑定 Activity 的 onDestroy,而是绑定一个 ViewModelStoreOwner(Activity / Fragment / Navigation Graph):
- 当 Activity 因配置变更销毁时,旧 Activity 的
ViewModelStore会被保留并转移到新 Activity; - 当 Activity 真正销毁(用户按返回键或系统回收)时,
ViewModelStore才被清空,调用ViewModel.onCleared()。
Activity 创建 ──> ViewModelStore 创建 ──> ViewModel 实例化
│
│ 配置变更(旋转)
↓
旧 Activity.onDestroy() ─> ViewModelStore 保留
↓
新 Activity.onCreate() ─> 复用同一 ViewModel
│
│ 用户真正退出
↓
Activity.onDestroy() ─> ViewModelStore.clear() ─> ViewModel.onCleared()
ViewModelStoreOwner 与获取 ViewModel
Activity、Fragment 都实现了 ViewModelStoreOwner,通过 ViewModelProvider 获取 ViewModel:
public class ProfileViewModel extends ViewModel {
private final MutableLiveData<User> user = new MutableLiveData<>();
public LiveData<User> getUser() { return user; }
public void load(long id) {
// 模拟异步加载
user.setValue(new User(id, "User-" + id));
}
@Override
protected void onCleared() {
super.onCleared();
// 释放资源:取消网络请求、关闭数据库连接等
}
}
public class ProfileActivity extends AppCompatActivity {
private ProfileViewModel viewModel;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_profile);
// 同一个 owner 在配置变更前后拿到同一个 ViewModel 实例
viewModel = new ViewModelProvider(this).get(ProfileViewModel.class);
viewModel.getUser().observe(this, user -> {
((TextView) findViewById(R.id.tv_name)).setText(user.name);
});
if (savedInstanceState == null) {
viewModel.load(1L); // 仅首次创建时加载
}
}
}
不要向 ViewModel 传递 View 引用
ViewModel 的生命周期可能比 Activity 长(配置变更场景),因此绝不能把 Activity / Fragment / View 作为构造参数传入 ViewModel,否则会持有已销毁的 Activity 引用,造成内存泄漏。
LiveData 概念
LiveData<T> 是一种 可观察的数据持有者,与普通观察者模式不同的是:
- 生命周期感知:观察者必须传入
LifecycleOwner(Activity / Fragment),LiveData 只在 Owner 处于STARTED之后(STARTED/RESUMED)才回调onChanged; - 自动取消订阅:Owner 销毁时自动断开观察,避免内存泄漏;
- 粘性数据:新观察者订阅时会立即收到最新一次数据,便于配置变更后立即恢复 UI;
- 主线程安全:
setValue必须在主线程调用,postValue可在任意线程异步切回主线程。
public class TimerViewModel extends ViewModel() {
private final MutableLiveData<Integer> seconds = new MutableLiveData<>(0);
public LiveData<Integer> getSeconds() { return seconds; }
public void tick() {
Integer cur = seconds.getValue();
seconds.setValue(cur == null ? 1 : cur + 1);
}
// 后台线程更新
public void tickFromBackground() {
new Thread(() -> {
try { Thread.sleep(1000); } catch (InterruptedException ignored) {}
seconds.postValue(seconds.getValue() == null ? 1 : seconds.getValue() + 1);
}).start();
}
}
LifecycleOwner 与观察者机制
LiveData.observe(LifecycleOwner, Observer) 内部会注册一个 LifecycleEventObserver,在 Owner 状态变为 DESTROYED 时自动移除观察者。你也可以直接使用 observeForever(Observer),但必须手动 removeObserver,否则会泄漏:
public class TimerActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_timer);
TimerViewModel vm = new ViewModelProvider(this).get(TimerViewModel.class);
vm.getSeconds().observe(this, sec -> {
((TextView) findViewById(R.id.tv_timer)).setText(String.valueOf(sec));
});
findViewById(R.id.btn_tick).setOnClickListener(v -> vm.tick());
}
}
LiveData 转换
Transformations.map
Transformations.map(source, mapFunction) 会在 source 变化时把值映射成新类型,返回新的 LiveData:
public class OrderViewModel extends ViewModel {
private final MutableLiveData<Double> price = new MutableLiveData<>(0.0);
public LiveData<Double> getPrice() { return price; }
// 把价格映射为格式化字符串
public LiveData<String> getFormatted() {
return Transformations.map(price, p -> "¥" + String.format("%.2f", p));
}
public void setPrice(double p) { price.setValue(p); }
}
Transformations.switchMap
switchMap 用于根据源值切换到不同的下游 LiveData(典型场景:根据 ID 查询数据库 / 网络,返回另一个 LiveData):
public class UserRepo {
public LiveData<User> loadUser(long id) {
MutableLiveData<User> live = new MutableLiveData<>();
new Thread(() -> {
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
live.postValue(new User(id, "User-" + id));
}).start();
return live;
}
}
public class UserViewModel extends ViewModel {
private final MutableLiveData<Long> userId = new MutableLiveData<>();
private final UserRepo repo = new UserRepo();
public void setUserId(long id) { userId.setValue(id); }
// 当 userId 变化时,切换到对应的 loadUser LiveData
public LiveData<User> getUser() {
return Transformations.switchMap(userId, repo::loadUser);
}
}
switchMap 会自动取消上一个内层 LiveData 的订阅,避免并发请求交叉污染。
MediatorLiveData
MediatorLiveData<T> 用于 同时观察多个源 LiveData,把它们的变更汇总到一个出口。典型用法:合并网络数据 + 数据库数据、合并多个请求状态。
public class DashboardViewModel extends ViewModel {
private final MediatorLiveData<String> summary = new MediatorLiveData<>();
private final MutableLiveData<String> networkData = new MutableLiveData<>();
private final MutableLiveData<String> dbData = new MutableLiveData<>();
public DashboardViewModel() {
summary.addSource(networkData, value -> summary.setValue("网络:" + value));
summary.addSource(dbData, value -> summary.setValue("数据库:" + value));
}
public LiveData<String> getSummary() { return summary; }
public void setNetworkData(String v) { networkData.setValue(v); }
public void setDbData(String v) { dbData.setValue(v); }
}
只有当某个源发生实际数据变更时才会触发 MediatorLiveData 的 setValue,调用方订阅 summary 即可。
ViewModel + LiveData 实战:数据加载
下面整合上面所有概念,做一个完整的“加载用户列表”示例,涵盖加载状态、错误处理、配置变更保留:
public class UserListState {
public final boolean loading;
public final List<User> data;
public final String error;
public UserListState(boolean loading, List<User> data, String error) {
this.loading = loading;
this.data = data;
this.error = error;
}
public static UserListState loading() {
return new UserListState(true, Collections.emptyList(), null);
}
public static UserListState success(List<User> data) {
return new UserListState(false, data, null);
}
public static UserListState error(String message) {
return new UserListState(false, Collections.emptyList(), message);
}
}
public class UserListViewModel extends ViewModel {
private final MediatorLiveData<UserListState> state = new MediatorLiveData<>();
private final UserRepo repo = new UserRepo();
public LiveData<UserListState> getState() { return state; }
public void refresh() {
LiveData<List<User>> source = repo.fetchUsers();
state.setValue(UserListState.loading());
// 用 MediatorLiveData 把 repo 的结果映射到统一 state
state.addSource(source, list -> {
state.setValue(UserListState.success(list));
state.removeSource(source); // 一次性,避免重复
});
}
@Override
protected void onCleared() {
super.onCleared();
// 实际项目应取消网络请求 / RxJava Disposable
}
}
public class UserListActivity extends AppCompatActivity {
private UserListViewModel viewModel;
private ProgressBar loadingPb;
private TextView errorTv;
private RecyclerView recyclerView;
private UserAdapter adapter;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_list);
loadingPb = findViewById(R.id.pb_loading);
errorTv = findViewById(R.id.tv_error);
recyclerView = findViewById(R.id.rv_users);
recyclerView.setLayoutManager(new LinearLayoutManager(this));
adapter = new UserAdapter();
recyclerView.setAdapter(adapter);
viewModel = new ViewModelProvider(this).get(UserListViewModel.class);
viewModel.getState().observe(this, s -> {
loadingPb.setVisibility(s.loading ? View.VISIBLE : View.GONE);
errorTv.setVisibility(s.error != null ? View.VISIBLE : View.GONE);
if (s.error != null) {
errorTv.setText(s.error);
}
if (s.data != null && !s.data.isEmpty()) {
adapter.submit(s.data);
}
});
if (savedInstanceState == null) {
viewModel.refresh();
}
findViewById(R.id.btn_refresh).setOnClickListener(v -> viewModel.refresh());
}
}
旋转屏幕后,UserListState 会被 ViewModel 保留,Activity 重新订阅时立即拿到上一次结果,无需重新发起网络请求。
常见坑与最佳实践
- 在 ViewModel 中持有 View / Activity 引用:会泄漏已销毁的 Activity。所有 UI 更新都通过 LiveData 抛出,由 View 订阅。
- 暴露 MutableLiveData 给外部:对外只暴露
LiveData<T>,避免外部随意setValue导致状态不可控。 - 在子线程调用 setValue:
setValue必须在主线程,子线程应使用postValue,否则抛IllegalStateException。 - switchMap 中返回新 LiveData 重复订阅:
switchMap内部自动管理订阅切换,不要在外部再手动addSource,否则会泄漏。 - 粘性数据导致重复事件:粘性机制会在新观察者订阅时立即回放最后一次数据,注意区分“重复触发”和“恢复 UI”。
- 在 onCleared 中启动异步清理:
onCleared在主线程同步执行,长任务应提前在 ViewModel 内部管理(如CompositeDisposable)。 - 用 observeForever 忘记 remove:尽量用
observe(LifecycleOwner, ...);非要observeForever时必须在合适的生命周期点removeObserver。
章节小结
本章覆盖了 ViewModel 的作用域与生命周期绑定规则、LiveData 的生命周期感知观察机制、Transformations.map / switchMap 转换、MediatorLiveData 多源合并,并用一个数据加载场景串联起所有概念。核心要点是:ViewModel 不持有 View 引用、LiveData 只暴露不可变接口、配置变更时数据由 ViewModel 自动保留。
下一章预告
下一章将进入依赖注入领域,使用 Dagger 2 完整实战:核心注解 @Inject / @Provides / @Module / @Component、@Singleton 与自定义 Scope、@Qualifier 限定符、Component 依赖与 Subcomponent、AndroidInjector(dagger.android)以及测试中替换 Module 的技巧。