一ViewModel(储存数据)
屏幕旋转,activity默认会销毁重新创建,这个时候页面上的数据就会消失,或系统内存不足,系统会杀死后台运行的页面,再次进入页面数据也会消失。这给用户带来了很不友好的体验。
在ViewModel出现之前,当页面旋转或因内存不足被系统杀死之前,会调用onSaveInstanceState方法,我们可以把我们想要保存的数据通过该方法保存下来,当页面再次创建的时候,我们就可以获取到之前保存的数据。这样的处理方式会让开发者编写很多保存和恢复数据的代码。官方注意到了这一点,提供了ViewModel库来解决这两个问题。
屏幕旋转保留数据的思路:把页面数据放入ViewModel类中,只要保证页面在重新创建的时候尝试获取老的ViewModel,如果没有再去创建新的ViewModel,并且在合适的时机清除保存老的ViewModel。(关键源码如下)
public <T extends ViewModel> T get(@NonNull String key, @NonNull Class<T> modelClass) {
ViewModel viewModel = mViewModelStore.get(key);
if (modelClass.isInstance(viewModel)) {
if (mFactory instanceof OnRequeryFactory) {
((OnRequeryFactory) mFactory).onRequery(viewModel);
}
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);
return (T) viewModel;
}
getLifecycle().addObserver(new LifecycleEventObserver() {
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (event == Lifecycle.Event.ON_DESTROY) {
if (!isChangingConfigurations()) {
getViewModelStore().clear();
}
}
}
});
通过源码可以看出:
1.ViewModel比页面生命周期要长,只有这样,才能获取老的对象
2.ViewModel使用了Lifecycle框架,毕竟要在合适的实际清除数据
2.仅上边源码并未找到处理页面意外被后台关闭数据保存的逻辑,事实也确实这样。
原理知道了,那么如何使用ViewModule呢?
1.继承ViewModel
2.把要保存的数据放入ViewModel中
3.页面上数据全部通过Viewmodel去保存和获取
代码如下:
class MyViewModule() : ViewModel() {
var number = 0
}
class MainActivity : AppCompatActivity() {
lateinit var viewModule: MyViewModule
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModule = ViewModelProvider(this).get(MyViewModule::class.java)
tv.setText(viewModule.number.toString())
button.setOnClickListener {
viewModule.number = 1
tv.setText(viewModule.number.toString())
}
}
}
注意:如果我们自己去设计ViewModule,那么我们就要自己处理获取 储存 清除。google给我们封装关于数据的获取储存清除,这就意味着,我们不能去new,而是用过google的api去获取ViewModel对象。
通过上边代码我们可以在旋转屏幕的时候数据不会重新创建。我们继续验证第二个理论:页面被系统意外销毁,数据是否会保存(开发者选项-->开启不保留活动)发现数据并未保存。
那么如何能让ViewModel保存页面被系统意外销毁,再次进入,数据仍然被保存呢?
步骤:
1.给自己定义的ViewModel构造函数中添加一个参数:val handle: SavedStateHandle
2.创建ViewModel对象的时候传入创建该对象的工厂类(可以不写)
代码如下:
class MyViewModule(val handle: SavedStateHandle) : ViewModel() {
val key = "number"
var number = 0
fun getNumber(): Student {
if (!handle.contains(key)){
handle.set(key,Student("默认值"))
}
return handle.get<Student>(key)!!
}
fun setNumber( stu:Student){
handle.set(key,stu)
}
}
class MainActivity : AppCompatActivity() {
lateinit var viewModule: MyViewModule
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModule = ViewModelProvider(this,SavedStateViewModelFactory(application,this)).get(MyViewModule::class.java)
tv.setText(viewModule.number.toString())
button.setText(viewModule.getNumber().name)
button.setOnClickListener {
viewModule.number = 1
tv.setText(viewModule.number.toString())
viewModule.setNumber(Student("张三"))
button.setText(viewModule.getNumber().name)
}
}
}
在页面被系统关闭,再次进入页面,button上的数据仍然会恢复,tv上的数据不会恢复。
tips1:步骤2不实现也可以,是因为系统默认使用的就是这个工厂类,ComponentActivity源码如下:
public ViewModelProvider.Factory getDefaultViewModelProviderFactory() {
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 (mDefaultFactory == null) {
mDefaultFactory = new SavedStateViewModelFactory(
getApplication(),
this,
getIntent() != null ? getIntent().getExtras() : null);
}
return mDefaultFactory;
}
tips2:当我们ViewModel需要一个上下文对象的时候,我们可以继承AndroidViewModel对象。
tips3:如果要保存对象类型,需要对象实现序列化
细心地人已经发现上边示例代码有点冗余:每次改变值需要改变两个地方:Viewmodel中的值和UI上边的值。并没有节省下来代码,google当然也注意到这点,并且为我们提供了贴心的解决方法LiveData。
二MutableLiveData
思路:利用监听者模式,把ViewModel中的数据用MutableLiveData包裹住,这样数据就可以设置监听,当数据改变后,会自动更新ui,类似数据驱动ui展示,我们只管改变数据即可。
代码如下:
class MyViewModule(val handle: SavedStateHandle) : ViewModel() {
val key = "number"
fun getNumber(): MutableLiveData<Student> {
if (!handle.contains(key)){
handle.set(key,Student("默认值"))
}
return handle.getLiveData(key)
}
fun setNumber(stu :Student){
getNumber().value = stu
}
}
class MainActivity : AppCompatActivity() {
lateinit var viewModule: MyViewModule
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
viewModule = ViewModelProvider(this,SavedStateViewModelFactory(application,this)).get(MyViewModule::class.java)
viewModule.getNumber().observe(this,object :Observer<Student>{
override fun onChanged(t: Student?) {
tv.setText(t?.name)
}
})
button.setOnClickListener {
viewModule.setNumber(Student("张三"))
}
}
}
tips1:MutableLiveData并没有能力发现name改变后进行数据的驱动,而是当对象整体改变后才会驱动,那么有没有更加细节的,当观察到对象中某个字段改变,就去驱动UI的更新呢?那就是使用MutableLiveData的父类,LiveData类,具体实现
tips2:ViewModel是Acitivy局部单例(因为ViewModel的储存位置决定了他局部单例的特性),我们可以通过ViewModel配合LiveData去实现Fragment之间的通讯,(注意Fragment写法:viewModule =ViewModelProvider(mActivity).get(HomeViewModule::class.java))
源码分析:
@Override
public void onStateChanged(@NonNull LifecycleOwner source,
@NonNull Lifecycle.Event event) {
if (mOwner.getLifecycle().getCurrentState() == DESTROYED) {
removeObserver(mObserver);
return;
}
activeStateChanged(shouldBeActive());
}
上线源码依赖Lifecycle来移除监听,防止内存泄漏。
@MainThread
protected void setValue(T value) {
assertMainThread("setValue");
mVersion++;
mData = value;
dispatchingValue(null);
}
setValue方法会触发被观察者回调,这也解释了为什么tips1某个字段改变无法触发回调的原因。
private void considerNotify(ObserverWrapper observer) {
if (!observer.mActive) {
return;
}
// Check latest state b4 dispatch. Maybe it changed state but we didn't get the event yet.
//
// we still first check observer.active to keep it as the entrance for events. So even if
// the observer moved to an active state, if we've not received that event, we better not
// notify for a more predictable notification order.
if (!observer.shouldBeActive()) {
observer.activeStateChanged(false);
return;
}
if (observer.mLastVersion >= mVersion) {
return;
}
observer.mLastVersion = mVersion;
observer.mObserver.onChanged((T) mData);
}
上边源码表达的:只有当页面是激活状态的时候,才触发回调,如果想要非激活状态也触发回调就需要使用observerForever方法。
三DataBinding
通过上边两个类的配合,我们代码执行逻辑变成如下图:
关于UI的更新还是通过持有references对象向View中更新,于是google提供了DataBinding来让ViewModel直接和View去绑定,但是通过很多前辈的实践证明DataBinding如果用于生产环境带来的痛苦比收益更大,故不打算用这个功能,这里不再阐述。Databinding吐槽
四Navigation
google提出了单页面应用(一个应用只有一个Activity,剩下的全部是Fragment),如果按照这个思路进行下去,程序员就会面临一个问题,Fragment回退栈,当然google为我们提供了自己控制栈的回退,又是一堆繁琐的代码。于是google帮我们封装了fragment需要使用回退栈的情况----Navigation。
封装细节一:假如加载三个Fragment,那么第一个Fragment永远不用进入栈。从第二个Fragment才进入栈管理。
封装细节二:每个Fragment进入,上一个Fragment都会触发生命周期方法onDestroyView执行,包括第一个Fragment。猜测内部源码如下:
加载第一个Fragment,不进行栈管理:
supportFragmentManager.beginTransaction().add(R.id.con,onefragment).commit()
从第二个Fragment开始就执行:
supportFragmentManager.beginTransaction().replace(R.id.con,TwoFragment()).addToBackStack("two").commit()
注意:View重新渲染,就会带来UI显示和数据的丢失。数据丢失,我们可以使用ViewModel来解决,UI显示数据丢失,我们可以通过android:saveEnabled属性来冻结(必须设置id)。毕竟View重新渲染了。
五Lifecycle
我们在使用Activity或者Fragment的时候,往往需要在生命周期中处理一些资源的获取和释放,如果写到声明周期方法中,会让代码变得臃肿,如果抽离出来,就需要自己再搞一个回调(MVP架构),google就从源码层做了统一的回调,让我们可以在自己创建的类获取想要观察到的声明周期。(本质还是向LifecycleRegistry类中注册监听,并且在生命周期回调的时候,触发LifecycleRegistry内部回调自己创建的监听)
使用方法:
public class MyLife implements LifecycleObserver {
@OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
private void MainOnCreate(){
Log.e("rrrrrrrrrrre","MainonCreate");
}
@OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
private void MainOnResume(){
Log.e("rrrrrrrrrrre","MainOnResume");
}
@OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
private void MainOnPause(){
Log.e("rrrrrrrrrrre","MainOnPause");
}
@OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
private void MainOnDestroy(){
Log.e("rrrrrrrrrrre","MainOnDestroy");
}
}
lifecycle.addObserver(MyLife())
或者
lifecycle.addObserver(object:LifecycleEventObserver{
override fun onStateChanged(source: LifecycleOwner, event: Lifecycle.Event) {
Log.e("rrrrrrrrrrre",event.toString())
}
})