Android:LiveData postValue导致数据丢失问题,及其原因
关于这个问题,网上很多,有一篇文章还详细列举了几种情况,写的非常直观:https://www.jianshu.com/p/aa24dd9123a1
我写的此文章比较多的个人想法,需要自己思考一下。
我碰到的实际情况是:
使用阿里RTC实时音视频服务,我把音视频操作和回调都写在了ViewModel中,在同一房间内,已经有人的情况下,在自己加入房间时,会触发阿里SDK事件通知回调onRemoteUserOnLineNotify,告诉我当前房间存在的人,因为回调都是在非主线程里,然后我通过LiveData.postValue通知到UI有人加入,我在recyclerView的adapter将数据add进去,有几个人回调几次此方法。在不止一人的情况下,就会几乎同时的多次调用LiveData.postValue,从而导致我只观察到了最后一个postValue。
我不想先讲这个问题的解决办法,我想先谈谈:为什么会出现这个问题?
据我猜测,碰到这个问题的大多数使用情况应该和我上面的差不多,都是获取数据,并且将数据添加到列表中。不知道猜的对不对?
那么出现这个问题的原因,是你对于LiveData的认知,是LiveData的概念问题。在你而言,LiveData是用于事件通知呢,还是一个activity的数据持有类。LiveData的正确使用方式是:
作为可以被观察的数据持有类
在MVP架构中,假如增加了功能,那么首先接口层需要增加一个方法定义,View层需要实现其方法,Presenter层调用此方法,把数据回调到UI界面上,其中需要判断activity是否被销毁。这样做能明显看出有2点不足,一是方法定义变多,改动的地方增多,而且实现接口从代码来看,不够直观、二是需要手动控制Presenter的生命周期。
那么在MVVM中,LiveData很好的解决了这个问题,我们不需要写一个接口文件,把方法提前定义好;也不需要自己判断数据更新时UI是否存在。只需要将需要的数据类型包裹在MutableLiveData中,生成它,在activity中观察:
viewModel.liveData.observe(this, new Observer() { @Override public void onChanged(xxx s) { 在这边实现数据的使用 }});
这个时候,对于我来说,概念上的偏差就来了,我是将它作为MVP中V和P之间交互的替代品,那么它就是作为一个数据通知功能。把它当成一种事件传递,数据通知的工具会出现什么问题呢?
接下来看一个简单的例子,来看看它作为界面数据持有的功能,功能很简单:界面上一个TextView,两个Button,一个按钮旋转屏幕,让activity重建,一个按钮生成String数据,并将数据设置到TextView上:
上代码,代码中使用了封装的框架,这个框架源自github上的一个项目,我拿来修改重新封装更适合自己使用,功能上大致能猜个八九不离十,之前想写这个框架博客的,但是太忙了。
首先界面 layout:
<?xml version="1.0" encoding="utf-8"?>
当然,LiveData也支持android DataBinding,写成android:text="@{viewModel.testLiveData}",只是为了更直观的观察它调用情况,不使用它。
ViewModel类:
public class TestViewModel extends BaseViewModel { public MutableLiveData testLiveData = new MutableLiveData<>(); public TestViewModel(@NonNull Application application) { super(application); } public void setTestString(String str) { Handler handler = new Handler(); handler.postDelayed(new Runnable() { @Override public void run() { testLiveData.postValue(str); } }, 1000); } @Override public void onCreate() { Log.d("TEST--activity", "onCreate"); } @Override public void onResume() { Log.d("TEST--activity", "onResume"); } @Override public void onPause() { Log.d("activity", "onPause"); } @Override public void onDestroy() { Log.d("TEST--activity", "onDestroy"); }}
因为我实现了LifecycleObserver接口方法,所以可以直接重写onCreate这些方法。然后setTestString模拟网络延时数据。
Acitivity类:
public class TestActivity extends BaseActivity { @Override protected void initData() { } @Override protected void initViewObservable() { viewModel.testLiveData.observe(this, new Observer() { @Override public void onChanged(String s) { Log.d("TEST--liveData", "---观察到了数据改变---"); binding.tvTest.setText(s); } }); binding.btSetText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { //binding.tvTest.setText("直接设置文字"); viewModel.setTestString("设置liveData数据,观察该LiveData,在其改变时,更新UI"); } }); binding.btChange.setOnClickListener(new View.OnClickListener() { @SuppressLint("SourceLockedOrientationActivity") @Override public void onClick(View v) { if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } else { setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE); } } }); } @Override public TestViewModel initViewModel() { return new ViewModelProvider(this, new ViewModelProvider.AndroidViewModelFactory(getApplication())) .get(TestViewModel.class); } @Override public int initContentView() { return R.layout.activity_test; } @Override public int initVariableId() { return BR.viewModel; }}
这个比较简单,我连Model都没放,单看initViewObservable方法,初始化了按钮的点击方法,观察了ViewModel的testLiveData,应该都比较简单。接下来看log,我的操作是,打开Activity,点击设置文字,再点击旋转屏幕
D/TEST--activity: onCreate ①D/TEST--activity: onResume ②D/TEST--liveData: ---观察到了数据改变--- ③D/TEST--activity: onDestroy ④D/TEST--activity: onCreate ⑤D/TEST--liveData: ---观察到了数据改变--- ⑥D/TEST--activity: onResume ⑦我标了个小圆圈数字,比较好说明一下,1和2是在activity启动触发的;3是点击了设置文字按钮后,触发了LiveData观察到的;4和5是屏幕旋转activity被重建;6是在重建时自动触发了LiveData观察7就不说明了
附图:
可以看出来,LiveData在activity重建时,会把数据重新赋予一次,这就是它本质的功能可以被观察的数据持有类,它持有着界面上的数据,那么在界面重建时,会把数据恢复。
那么再看,假如不用LiveData呢,现在把那个设置文字按钮点击事件更换一下:
binding.btSetText.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { binding.tvTest.setText("直接设置文字"); //viewModel.setTestString("设置liveData数据,观察该LiveData,在其改变时,更新UI"); } });
可以看到,旋转屏幕时,数据丢失了。
因此,LiveData并不是简单的用于事件通知和数据回调。假设像上面RTC例子中我没碰到数据丢失的情况,他们进房间都是一个一个进的,就不会有问题,但是RTC实时音视频界面被重建了,这个时候,LiveData恢复的数据,肯定只有最后进房间的那个人的数据。同样的道理,假如把LiveData当做比如RecyclerView加载更多的数据回调,在界面重建时,恢复的也是部分数据。
这个合理吗?我觉得是合理的,LiveData所持有的数据,就是界面上要展示的数据,最后一次postValue就是你界面上应该展示的数据,所以中间的数据都没发送出去。看看它的源码:
protected void postValue(T value) { boolean postTask; synchronized (mDataLock) { postTask = mPendingData == NOT_SET; mPendingData = value; } if (!postTask) { return; } ArchTaskExecutor.getInstance().postToMainThread(mPostValueRunnable); }
在这个runnable未被执行前,多次调用postValue,mPendingData就会被多次赋值,所以只有最后一次数据被发送出去了。
解决办法:
1、使用setValue,setValue不会造成数据丢失,它每次都会调用,但是这样会有界面重建时数据丢失的隐患。(去除这个隐患的话,就getValue 然后获取到的数据,add,再setValue)
2、保证数据不会一起进来,大部分数据应该都不会同时进来的,所以碰到这种问题的比较小众。
(不能使用getValue获取列表项再add数据,然后postValue的方法,getValue获取的数据是setValue后的mData,在还没被调用到setValue时,你getValue出来的数据,都是在postValue之前的数据)
更多相关文章
- “罗永浩抖音首秀”销售数据的可视化大屏是怎么做出来的呢?
- Nginx系列教程(三)| 一文带你读懂Nginx的负载均衡
- 不吹不黑!GitHub 上帮助人们学习编码的 12 个资源,错过血亏...
- [sg] Android(安卓)6.0 添加对Home键的拦截
- [android]获取各应用的启动次数和运行时间
- 【Android】安卓AVD无法上网解决方案
- Android(安卓)Camera API2中采用CameraMetadata用于从APP到HAL的
- android事件拦截处理机制图解
- android TIPS小结