前言
在引入了 Jetpack 之后,我们通常使用 ViewModel 组件来管理数据,当页面因配置变更(尤其是在发生像旋转这样频繁的配置更改之后)而重建时,可以使用 ViewModel 和 onSaveInstanceState(),以确保应用满足用户对其界面状态的预期。
但如果是内存不足或者电量不足等系统原因导致的页面被回收时 ViewModel 是不会被复用的。
一、用户预期和系统行为
参考官网 :https://developer.android.google.cn/topic/libraries/architecture/saving-states
1、用户发起的界面状态解除
用户希望当他们启动 Activity 时,该 Activity 的暂时性界面状态会保持不变,直到用户完全关闭 Activity 为止。用户可以通过以下方式完全关闭 Activity:
按返回按钮
从“概览”(“最近使用的应用”)屏幕中滑动关闭 Activity
从 Activity 向上导航
从“设置”屏幕中终止应用
完成某种“完成”Activity(由 Activity.finish() 提供支持)
以上情况 Activity 实例将连同其中存储的任何状态以及与该 Activity 关联的任何已保存实例状态记录一起被销毁并从内存中移除。
2、系统发起的界面状态解除
用户期望 Activity 的界面状态在整个配置变更(例如旋转或切换到多窗口模式)期间保持不变。
但是,默认情况下,系统会在发生此类配置更改时销毁 Activity;
例如应用切换至后台后重新回到App时内存不足导致App被重启;
我们知道Activity可以通过 onSaveInstanceState() onRestoreInstanceState()方式 状态保存机制,页面重建后 ,可以恢复之前的状态,为用户提供更好的体验。
onSaveInstanceState() 回调会存储一些数据,如果系统销毁后又重新创建界面控制器(如 Activity 或 Fragment),则需要使用这些数据重新加载该控制器的状态。
因为 onSavedInstanceState() 会将数据序列化到磁盘。如果序列化的对象很复杂,序列化会占用大量的内存。
所以onSavedInstanceState() 不能用于存储大量的数据(如位图),也不能用于存储冗长复杂数据结构
而是只能用于存储基本类型和简单的小对象,例如字符串
二、内存不足或者电量不足等系统原因导致的页面被回收时 ViewModel 不会被复用的情况
模拟购买商品时增加/减少数量的场景:
1、创建ViewModel,添加add()&del()控制num变化
public class MyNormalViewModel extends ViewModel {
private MutableLiveData<Integer> num = new MutableLiveData<Integer>(1);
public LiveData<Integer> getNumber(){
return num;
}
public void add(){
num.setValue((int)num.getValue() + 1);
}
public void del(){
if(num.getValue() > 1){
num.setValue((int)num.getValue() - 1);
}
}
}
2、创建Activity,通过DataBinding控制UI数据
public class MyActivity extends AppCompatActivity {
MyNormalViewModel myViewModel;
MainActivityMyBinding binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
binding = DataBindingUtil.setContentView(this, R.layout.main_activity_my);
myViewModel = new ViewModelProvider(this).get(MyNormalViewModel.class);
binding.setVm(myViewModel);
binding.setLifecycleOwner(this);
}
}
3、创建布局视图,通过DataBinding控制视图属性
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.pxwx.main.viewmodel.MyNormalViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="@dimen/dp_30"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnDecrease"
android:layout_width="30dp"
android:layout_height="30dp"
android:gravity="center"
android:text="-"
android:textStyle="bold"
android:background="#999999"
android:onClick="@{()->vm.del()}"/>
<EditText
android:id="@+id/etAmount"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="50dp"
android:background="@null"
android:inputType="number"
android:gravity="center"
android:enabled="false"
android:textColor="@color/color_222222"
android:text="@{vm.number.toString()}"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnIncrease"
android:layout_width="30dp"
android:layout_height="30dp"
android:gravity="center"
android:text="+"
android:textStyle="bold"
android:background="#999999"
android:onClick="@{()->vm.add()}"/>
</LinearLayout>
</RelativeLayout>
</layout>
模拟购买商品时增加/减少数量的场景步骤:
1、增加要兑换的商品数量
2、开发者选项打开【不保留活动】
3、按home键到桌面后再从进程中回到app
4、再进行加减操作时数量是重置状态了
通过ViewModel该如何解决呢?
应用切换至后台后重新回到App时内存不足导致App被重启;为了它能够帮助开发者在 ViewModel 中处理 Activity 和 fragment 状态保存和恢复,googole提供了viewmodel-savedstate组件
三、ViewModel-SavedState保存数据
1、首先先添加依赖
api 'androidx.lifecycle:lifecycle-viewmodel-savedstate:2.2.0'
2、ViewModel 搭配 SavedState 实现数据复用
ViewModel 可以有一个接收 SavedStateHandle 的构造函数:
public class MySavedStateViewModel extends ViewModel {
private SavedStateHandle handle;
public static final String KEY = "KEY_NUMBER";
public MySavedStateViewModel(SavedStateHandle handle){
if (!handle.contains(KEY)) {
handle.set(KEY, 0);
}
this.handle = handle;
}
public LiveData<Integer> getNumber(){
return handle.getLiveData(KEY);
}
public void add(){
handle.set(KEY,(int)handle.get(KEY) + 1);
}
public void del(){
handle.set(KEY,(int)handle.get(KEY) - 1);
}
}
3、通过传入savedstateviewmodel创建viewmodel
public class MyActivity extends AppCompatActivity {
MySavedStateViewModel myViewModel;
MainActivityMyBinding binding;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
//用databinding来绑定布局
binding = DataBindingUtil.setContentView(this, R.layout.main_activity_my);
//通过传入savedstateviewmodel创建viewmodel
myViewModel = new ViewModelProvider(this,new SavedStateViewModelFactory(getApplication(),this)).get(MySavedStateViewModel.class);
binding.setVm(myViewModel);
binding.setLifecycleOwner(this);
}
}
4、布局文件处理,同上面的列子一样,不变
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:tools="http://schemas.android.com/tools"
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<data>
<variable
name="vm"
type="com.pxwx.main.viewmodel.MySavedStateViewModel" />
</data>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center">
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="@dimen/dp_30"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnDecrease"
android:layout_width="30dp"
android:layout_height="30dp"
android:gravity="center"
android:text="-"
android:textStyle="bold"
android:background="#999999"
android:onClick="@{()->vm.del()}"/>
<EditText
android:id="@+id/etAmount"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:minWidth="50dp"
android:background="@null"
android:inputType="number"
android:gravity="center"
android:enabled="false"
android:textColor="@color/color_222222"
android:text="@{vm.number.toString()}"/>
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btnIncrease"
android:layout_width="30dp"
android:layout_height="30dp"
android:gravity="center"
android:text="+"
android:textStyle="bold"
android:background="#999999"
android:onClick="@{()->vm.add()}"/>
</LinearLayout>
</RelativeLayout>
</layout>
再次模拟购买商品时增加/减少数量的场景步骤:
1、增加要兑换的商品数量
2、开发者选项打开【不保留活动】
3、按home键到桌面后再从进程中回到app
4、再进行加减操作时数量是在原来保存的数据基础上增加(正常)
SavedStateHandle 类包含键值对映射应有的方法:
get(String key)
contains(String key)
remove(String key)
set(String key, T value)
keys()
如上使用:此外,还有一种特殊的方法:getLiveData(String key),用于返回封装在 LiveData 可观察对象中的值。
当使用Savedstate保存数据之后,后台进程关闭,数据也会得到保留