当前位置: 代码迷 >> 综合 >> Android:LiveData postValue导致数据丢失问题,及其原因
  详细解决方案

Android:LiveData postValue导致数据丢失问题,及其原因

热度:36   发布时间:2024-02-09 02:28:19.0

关于这个问题,网上很多,有一篇文章还详细列举了几种情况,写的非常直观: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<String>() {@Overridepublic void onChanged(xxx s) {在这边实现数据的使用}
}); 

这个时候,对于我来说,概念上的偏差就来了,我是将它作为MVP中V和P之间交互的替代品,那么它就是作为一个数据通知功能。把它当成一种事件传递,数据通知的工具会出现什么问题呢?

接下来看一个简单的例子,来看看它作为界面数据持有的功能,功能很简单:界面上一个TextView,两个Button,一个按钮旋转屏幕,让activity重建,一个按钮生成String数据,并将数据设置到TextView上:

上代码,代码中使用了封装的框架,这个框架源自github上的一个项目,我拿来修改重新封装更适合自己使用,功能上大致能猜个八九不离十,之前想写这个框架博客的,但是太忙了。

首先界面 layout:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"xmlns:app="http://schemas.android.com/apk/res-auto"xmlns:tools="http://schemas.android.com/tools"><data><variablename="viewModel"type="com.zh.mvvmui.viewmodel.TestViewModel" /></data><androidx.constraintlayout.widget.ConstraintLayoutandroid:layout_width="match_parent"android:layout_height="match_parent"tools:context=".mvvmui.activity.TestActivity"><TextViewandroid:id="@+id/tv_test"android:layout_width="wrap_content"android:layout_height="wrap_content"android:textSize="15sp"android:textColor="@color/black"android:layout_marginTop="20dp"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toTopOf="@+id/bt_change"/><Buttonandroid:id="@+id/bt_change"android:layout_width="120dp"android:layout_height="40dp"android:text="旋转屏幕"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toTopOf="parent"app:layout_constraintBottom_toBottomOf="parent" /><Buttonandroid:id="@+id/bt_set_text"android:layout_width="120dp"android:layout_height="40dp"android:text="设置文字"app:layout_constraintStart_toStartOf="parent"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintTop_toBottomOf="@+id/bt_change"app:layout_constraintBottom_toBottomOf="parent" /></androidx.constraintlayout.widget.ConstraintLayout>
</layout>

当然,LiveData也支持android DataBinding,写成android:text="@{viewModel.testLiveData}",只是为了更直观的观察它调用情况,不使用它。

ViewModel类:

public class TestViewModel extends BaseViewModel {public MutableLiveData<String> testLiveData = new MutableLiveData<>();public TestViewModel(@NonNull Application application) {super(application);}public void setTestString(String str) {Handler handler = new Handler();handler.postDelayed(new Runnable() {@Overridepublic void run() {testLiveData.postValue(str);}}, 1000);}@Overridepublic void onCreate() {Log.d("TEST--activity", "onCreate");}@Overridepublic void onResume() {Log.d("TEST--activity", "onResume");}@Overridepublic void onPause() {Log.d("activity", "onPause");}@Overridepublic void onDestroy() {Log.d("TEST--activity", "onDestroy");}
}

因为我实现了LifecycleObserver接口方法,所以可以直接重写onCreate这些方法。然后setTestString模拟网络延时数据。

 

Acitivity类:

public class TestActivity extends BaseActivity<ActivityTestBinding, TestViewModel> {@Overrideprotected void initData() {}@Overrideprotected void initViewObservable() {viewModel.testLiveData.observe(this, new Observer<String>() {@Overridepublic void onChanged(String s) {Log.d("TEST--liveData", "---观察到了数据改变---");binding.tvTest.setText(s);}});binding.btSetText.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {//binding.tvTest.setText("直接设置文字");viewModel.setTestString("设置liveData数据,观察该LiveData,在其改变时,更新UI");}});binding.btChange.setOnClickListener(new View.OnClickListener() {@SuppressLint("SourceLockedOrientationActivity")@Overridepublic void onClick(View v) {if (getRequestedOrientation() == ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE) {setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);} else {setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);}}});}@Overridepublic TestViewModel initViewModel() {return new ViewModelProvider(this,new ViewModelProvider.AndroidViewModelFactory(getApplication())).get(TestViewModel.class);}@Overridepublic int initContentView() {return R.layout.activity_test;}@Overridepublic 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() {@Overridepublic 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之前的数据)

  相关解决方案