核心区别:设计目的与生命周期
特性 | ViewModel | onSaveInstanceState |
---|---|---|
设计目的 | 持有和管理UI相关的数据。 | 保存和恢复短暂的UI状态。 |
数据范围 | “大”数据:复杂对象、列表、网络请求结果、数据库查询结果等。 | “小”数据:简单的、可序列化/可打包的数据(Primitive类型、String、Parcelable等)。 |
生命周期 | 存活于配置更改(如屏幕旋转)。不存活于进程终止(如用户离开应用后系统为回收内存而杀死应用)。 | 存活于配置更改和进程终止。数据被写入磁盘(Bundle),即使应用被杀死也能恢复。 |
存储位置 | 内存(RAM)中。访问速度极快。 | 序列化后写入磁盘(Bundle)。读写有开销。 |
适用场景 | 保持屏幕旋转时的数据;在Fragment间共享数据;作为数据层的“前台缓存”。 | 保存滚动位置、文本框中的临时输入、选中的ID等,确保即使在应用被杀死后也能完美还原用户体验。 |
一个绝佳的类比:
想象在电脑上写文档。
- ViewModel 就像 RAM。正在编辑的整个文档都在里面,操作非常快。但如果突然断电(进程被杀死),所有没保存的东西就丢了。
- onSaveInstanceState 就像 按 Ctrl+S 快速保存。不会保存整个文档,而是会保存一些关键信息(比如光标位置、最近编辑的段落)。即使断电,重启后也能迅速恢复到刚才的状态。
最佳实践与使用方法
核心思想:二者不是替代关系,而是互补关系。 应该结合使用它们来打造最佳用户体验。
使用
ViewModel
作为数据的“单一信源”:- 所有核心数据(从Repository、UseCase获取的数据)都应存放在
ViewModel
中。 - UI(Activity/Fragment)观察
ViewModel
暴露的LiveData
/StateFlow
来更新界面。
- 所有核心数据(从Repository、UseCase获取的数据)都应存放在
使用
onSaveInstanceState
作为UI状态的“备份”:- 仅用它来保存那些重建UI所必需的最小化状态信息,而不是完整的数据对象。
- 例如,保存一个项目的ID,而不是整个项目对象。在重建时,用这个ID从
ViewModel
中重新获取完整数据。
代码详解与示例
假设有一个 “用户详情”页面(UserDetailActivity),从网络加载用户数据,并有一个可滚动的TextView
。
1. ViewModel 代码 (UserDetailViewModel.java)
import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.ViewModel;
public class UserDetailViewModel extends ViewModel {
// 使用 LiveData 或 StateFlow 持有数据(核心数据)
private MutableLiveData<User> _user = new MutableLiveData<>();
public LiveData<User> user = _user;
private MutableLiveData<Boolean> _isLoading = new MutableLiveData<>();
public LiveData<Boolean> isLoading = _isLoading;
private String userId; // 要加载的用户ID
// 初始化加载数据
public void init(String userId) {
if (this.userId != null) {
// ViewModel 已经初始化过,配置旋转时直接使用现有数据
return;
}
this.userId = userId;
loadUser(userId);
}
private void loadUser(String userId) {
_isLoading.setValue(true);
// 模拟网络请求
UserRepository.getUser(userId, new Callback<User>() {
@Override
public void onSuccess(User user) {
_isLoading.setValue(false);
_user.setValue(user); // 数据加载成功,更新LiveData
}
@Override
public void onError(Exception e) {
_isLoading.setValue(false);
// 处理错误
}
});
}
// 清空资源(可选)
@Override
protected void onCleared() {
super.onCleared();
// 取消正在进行的网络请求等
}
}
2. Activity 代码 (UserDetailActivity.java)
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.lifecycle.ViewModelProvider;
import android.os.Bundle;
import android.widget.TextView;
public class UserDetailActivity extends AppCompatActivity {
private static final String SAVE_STATE_USER_ID = "SAVE_STATE_USER_ID";
private static final String SAVE_STATE_SCROLL_POSITION = "SAVE_STATE_SCROLL_POSITION";
private UserDetailViewModel viewModel;
private TextView userBioTextView;
private String userId;
private int scrollPosition = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_user_detail);
userBioTextView = findViewById(R.id.tv_user_bio);
// 1. 从 Intent 或 SavedState 中获取 userId
if (savedInstanceState == null) {
// 正常启动:从Intent获取
userId = getIntent().getStringExtra("USER_ID");
} else {
// 重建恢复:从Bundle获取(可能是进程被杀死后)
userId = savedInstanceState.getString(SAVE_STATE_USER_ID);
scrollPosition = savedInstanceState.getInt(SAVE_STATE_SCROLL_POSITION, 0);
}
// 2. 初始化 ViewModel
viewModel = new ViewModelProvider(this).get(UserDetailViewModel.class);
viewModel.init(userId); // 传递ID,ViewModel内部会判断是否已加载
// 3. 观察 ViewModel 中的数据
viewModel.user.observe(this, user -> {
if (user != null) {
// 更新UI with user data
userBioTextView.setText(user.getBio());
// 恢复滚动位置
userBioTextView.scrollTo(0, scrollPosition);
}
});
viewModel.isLoading.observe(this, isLoading -> {
// 显示或隐藏加载进度条
});
}
// 4. 保存UI状态(用于进程终止)
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
// 只保存最小必要信息:用户ID和滚动位置
outState.putString(SAVE_STATE_USER_ID, userId);
outState.putInt(SAVE_STATE_SCROLL_POSITION, userBioTextView.getScrollY()); // 获取当前滚动位置
}
}
两种场景下的工作流程
场景一:屏幕旋转(配置更改)
- 旋转前:Activity被销毁,
onSaveInstanceState
被调用,userId
和scrollPosition
被保存到Bundle中。 - 旋转后:新Activity创建,
onCreate(Bundle savedInstanceState)
被调用,savedInstanceState
不为null
。 - 恢复数据:从Bundle中取出
userId
和scrollPosition
。 - 获取核心数据:
ViewModel
没有被销毁,它仍然持有完整的User
对象。新Activity获取到同一个ViewModel
实例,立即观察到User
数据并更新UI,然后应用滚动位置。- 结果:用户体验无缝衔接,没有重复的网络请求。
场景二:应用被系统杀死后恢复
- 被杀前:
onSaveInstanceState
被调用,userId
和scrollPosition
被写入磁盘。 - 被杀后:用户重新打开应用,系统重新创建Activity,并将保存的Bundle传入
onCreate
。 - 恢复数据:从Bundle中取出
userId
和scrollPosition
。 - 获取核心数据:
ViewModel
是全新的、空的。viewModel.init(userId)
被调用,根据保存的userId
发起新的网络请求来获取完整的用户数据。 - 数据加载成功后,更新UI并滚动到之前的位置。
- 结果:用户看到了和离开时几乎一样的界面,但需要等待数据重新加载。
总结与最佳方法
永远使用
ViewModel
:- 用来持有所有非UI状态的核心数据。
- 防止因配置更改导致的不必要数据重载(如网络请求、数据库查询)。
谨慎使用
onSaveInstanceState
:- 只用来保存恢复UI所必需的、轻量的、可序列化的状态(ID、位置、临时文本)。
- 为应对进程死亡的场景提供保障。
分工合作:
ViewModel
保证配置更改时体验流畅。onSaveInstanceState
+ViewModel
共同保证进程死亡后体验连贯。
不要滥用:
- 切勿将大型对象(如Bitmap)或复杂结构放入Bundle,这会导致
TransactionTooLargeException
。 - 如果UI状态非常复杂,考虑使用SavedStateHandle(与ViewModel搭配使用),它简化了保存状态的过程。
- 切勿将大型对象(如Bitmap)或复杂结构放入Bundle,这会导致