ViewModel
的 Saved State
在屏幕旋转时,ViewModel
可以保存数据。但是当应用在后台进程被系统杀死,当重新打开页面时,ViewModel
的数据并不会恢复。这种情况就需要与SavedStateHandle
结合,在后台进程回收时保存数据。
第一步:添加依赖
def lifecycle_version = "2.2.0"
implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version
第二步:在 Activity 或 Fragment 的 onCreate 方法中,将 ViewModelProvider 的调用修改为:
//androidx.fragment:fragment-ktx:x.y.z 或
//androidx.activity:activity-ktx:x.y.z
val viewModel by viewModels { SavedStateViewModelFactory(application, this) }
// 或者不使用 ktx
val viewModel = ViewModelProvider(this, SavedStateViewModelFactory(application, this))
.get(MyViewModel::class.java)
第三步:使用 SaveStateHandle
class WithSavedStateViewModel(private val state: SavedStateHandle) : ViewModel() {
private val key = "key"
fun setValue(value: String) = state.set(key, value)
fun getValue(): LiveData = state.getLiveData(key)
}
class ViewModelWithSavedStateFragment :
BaseFragment(R.layout.fragment_viewmodel) {
private val mViewModel by viewModels()
override fun initBinding(view: View): FragmentViewmodelBinding =
FragmentViewmodelBinding.bind(view)
override fun init(savedInstanceState: Bundle?) {
binding.save.setOnClickListener { mViewModel.setValue(binding.edit.text.toString()) }
mViewModel.getValue().observe(viewLifecycleOwner) {
binding.text.text = getString(R.string.saved_in_viewmodel, it)
}
}
}
ViewMode
与 Kotlin 协程:viewModelScope
真实的使用环境下很容易创建出许多协程,这就难免会导致有些协程的状态无法被跟踪。如果这些协程中刚好有您想要停止的任务时,就会导致任务泄漏(work leak)。为了防止任务泄漏,您需要将协程加入到一个 CoroutineScope
中。CoroutineScope
可以持续跟踪协程的执行,它可以被取消。当 CoroutineScope
被取消时,它所跟踪的所有协程都会被取消。正如我们不推荐随意使用全局变量一样,GlobalScope
这种方式通常不推荐使用。所以,如果想要使用协程,您要么限定一个作用域 (scope),要么获得一个作用域的访问权限。而在 ViewModel
中,我们可以使用 viewModelScope
来管理协程的作用域。假设您正在准备将一个位图 (bitmap) 显示到屏幕上。这种操作就符合我们前面提到的一些特征:既不能在执行时阻塞主线程,又要求在用户退出相关界面时停止执行。使用协程进行此类操作时,就应当使用 viewModelScope
。
class MyViewModel() : ViewModel() {
fun doSth() {
viewModelScope.launch {
processBitmap()
}
}
suspend fun processBitmap() = withContext(Dispatchers.Default) {
// TODO: handle logic
}
}
几种数据恢复方式的总结:
1,当您的 Activity
开始停止时,系统会调用 onSaveInstanceState()
方法,以便您的 Activity 可以将状态信息保存到实例状态 Bundle
中。重建先前被销毁的 Activity 后,您可以从系统传递给 Activity 的 Bundle 中恢复保存的实例状态。onCreate()
和 onRestoreInstanceState()
回调方法均会收到包含实例状态信息的相同 Bundle
。
2,当配置发生改变时,Fragment 会随着宿主 Activity 销毁与重建,当我们调用 Fragment 中的 setRetainInstance(true)
方法时,系统允许 Fragment 绕开销毁-重建 的过程。使用该方法,将会发送信号给系统,让 Activity 重建时,保留 Fragment 的实例。需要注意的是:
使用该方法后,不会调用 Fragment 的
onDestory()
方法,但仍然会调用onDetach()
方法;使用该方法后,不会调用 Fragment 的
onCreate(Bundle)
方法。因为 Fragment 没有被重建;使用该方法后,Fragment 的
onAttach(Activity)
与onActivityCreated(Bundle)
方法仍然会被调用。
3,在 Activity 中提供了 onRetainNonConfigurationInstance()
方法,用于处理配置发生改变时数据的保存。随后在重新创建的 Activity 中调用 getLastNonConfigurationInstance()
获取上次保存的数据。我们不能直接重写上述方法,如果想在 Activity 中自定义想要恢复的数据,需要我们调用上述两个方法的内部方法:
注意:
onRetainNonConfigurationInstance()
方法系统调用时机介于onStop() - onDestory()
之间,getLastNonConfigurationInstance()
方法可在onCreate() 与 onStart()
方法中调用。
onRetainCustomNonConfigurationInstance()
getLastCustomNonConfigurationInstance()
为什么旋转手机时,ViewModel
可以保存数据?
Activity 因旋转发生改变时,系统会重新创建一个新的 Activity 。那老的 Activity 中的 ViewModel 是如何传递给新的 Activity 的呢?
ViewModel
在官方设计之初就倾向于在配置改变时进行数据的恢复。考虑到数据恢复时的效率,官方最终采用了onRetainNonConfigurationInstance()
的方式来恢复ViewModel
。
在 Androidx 中的 Activity 的最新代码中,官方重写了 onRetainNonConfigurationInstance()
方法,在该方法中保存了 ViewModelStore
(ViweModelStore
中存储了 ViewModel
),进而也保存了 ViewModel
,具体代码如下所示:
public final Object onRetainNonConfigurationInstance() {
Object custom = onRetainCustomNonConfigurationInstance();
ViewModelStore viewModelStore = mViewModelStore;
if (viewModelStore == null) {
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
viewModelStore = nc.viewModelStore;
}
}
if (viewModelStore == null && custom == null) {
return null;
}
//将ViewModel存储在 NonConfigurationInstances 对象中
NonConfigurationInstances nci = new NonConfigurationInstances();
nci.custom = custom;
nci.viewModelStore = viewModelStore;
return nci;
}
于是我们知道,在屏幕旋转后,当新的 Activity 重新创建,并调用 ViewModelProviders.of(this).get(xxxModel.class)
时,又会在 getViewModelStore()
方法中获取老 Activity 保存的 ViewModelStore
。
public ViewModelStore getViewModelStore() {
if (getApplication() == null) {
throw new IllegalStateException("Your activity is not yet attached to the "
+ "Application instance. You can't request ViewModel before onCreate call.");
}
if (mViewModelStore == null) {
//获取保存的NonConfigurationInstances,
NonConfigurationInstances nc =
(NonConfigurationInstances) getLastNonConfigurationInstance();
if (nc != null) {
//从该对象中获取ViewModelStore
mViewModelStore = nc.viewModelStore;
}
if (mViewModelStore == null) {
mViewModelStore = new ViewModelStore();
}
}
return mViewModelStore;
}
而 ViewModel
实际是存储在 ViewModelStore
中的,ViewModelStore
还原后,那么也就拿到了 ViewModel
。具体代码如下所示: 从 ViewModelStroe
中获取 ViewModel
的相关代码:
@NonNull
@MainThread
public T get(@NonNull String key, @NonNull Class modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
//noinspection unchecked
return (T) viewModel;
} else {
//noinspection StatementWithEmptyBody
if (viewModel != null) {
// TODO: log a warning.
}
}
if (mFactory instanceof KeyedFactory) {
viewModel = ((KeyedFactory) (mFactory)).create(key, modelClass);
} else {
viewModel = (mFactory).create(modelClass);
}
mViewModelStore.put(key, viewModel);
//noinspection unchecked
return (T) viewModel;
}
这就是为什么屏幕旋转后,ViewModel
可以保存数据的原因。
为什么 Fragment 中的数据屏幕旋转后可以保存?
如果我们在 Fragment 中调用如下代码:
val model: MyViewModel = ViewModelProviders.of(this).get(MyViewModel::class.java)
追踪他们的调用,可以发现获取 ViewModel
是通过 mNonConfig
存储的:
@NonNull
ViewModelStore getViewModelStore(@NonNull Fragment f) {
return mNonConfig.getViewModelStore(f);
}
那么 mNonConfig
又是什么时候创建的呢?又存储在哪里?
在将
Fragment
添加到FragmentManager
中时会调用到下面的函数,
因为传入的parent = null
,且Activity
默认实现了ViewModelStoreOwner
接口,所以会获取Activity
中的ViewModelStore
,接着调用 FragmentManagerViewModel 的 getInstance() 方法:
void attachController(@NonNull FragmentHostCallback> host,
@NonNull FragmentContainer container, @Nullable final Fragment parent) {
//省略更多...
if (parent != null) {
mNonConfig = parent.mFragmentManager.getChildNonConfig(parent);
} else if (host instanceof ViewModelStoreOwner) {
//走这里
ViewModelStore viewModelStore = ((ViewModelStoreOwner) host).getViewModelStore();
mNonConfig = FragmentManagerViewModel.getInstance(viewModelStore);
} else {
mNonConfig = new FragmentManagerViewModel(false);
}
}
// 创建 FragmentManagerViewModel,并将其添加到 Activity 中的 ViewModelStore 中
static FragmentManagerViewModel getInstance(ViewModelStore viewModelStore) {
ViewModelProvider viewModelProvider = new ViewModelProvider(viewModelStore,
FACTORY);
return viewModelProvider.get(FragmentManagerViewModel.class);
}
调用流程如下:
Activity #onCreate() -> mFragments #attachHost(null) -> FragmentManager #attachController -> (创建 FragmentManagerViewModel,并将其添加到 Activity 中的 ViewModelStore 中。)
ViewModel
在 Fragment 中不会因配置改变而销毁的原理
根据上面的分析,ViewModel 在 Fragment 中不会因配置改变而销毁的原因其实是因为其声明的 ViewModel 是存储在 FragmentManagerViewModel 中的,而 FragmentManagerViewModel 是存储在宿主 Activity 中的 ViewModelStore 中,又因 Activity 中 ViewModelStore 不会因配置改变而销毁,故 Fragment 中 ViewModel 也不会因配置改变而销毁。
ViewModel 能在 Fragment 中共享的原理
ViewModel 的另一大特性就是能在 Fragment 中共享数据。假如我们想 Fragment D 获取 Fragment A 中的数据,那么我们只有在 Activity 中的 ViewModelStore 下添加 ViewModel。只有这样,我们才能在不同 Fragment 中获取相同的数据。这也是为什么在 Fragment 中使用共享的 ViewModel 时,我们要在调用ViewModelProvider.of() 创建 ViewModel 时需要传入 getActivity() 的原因。
public class SharedViewModel extends ViewModel {
private final MutableLiveData- selected = new MutableLiveData
- ();
public void select(Item item) {
selected.setValue(item);
}
public LiveData
- getSelected() {
return selected;
}
}
public class FragmentA extends Fragment {
private SharedViewModel model;
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//传入的是宿主 Activity
model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
itemSelector.setOnClickListener(item -> {
model.select(item);
});
}
}
public class FragmentD extends Fragment {
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//传入的是宿主Activity
SharedViewModel model = ViewModelProviders.of(getActivity()).get(SharedViewModel.class);
model.getSelected().observe(this, { item ->
// Update the UI.
});
}
}
感谢:
https://flywith24.gitee.io/2020/03/23/Jetpack-ViewModel/