关于这个问题,网上很多,有一篇文章还详细列举了几种情况,写的非常直观: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>() {
@Override
public 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>
<variable
name="viewModel"
type="com.zh.mvvmui.viewmodel.TestViewModel" />
</data>
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".mvvmui.activity.TestActivity">
<TextView
android: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"/>
<Button
android: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" />
<Button
android: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() {
@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<ActivityTestBinding, TestViewModel> {
@Override
protected void initData() {
}
@Override
protected void initViewObservable() {
viewModel.testLiveData.observe(this, new Observer<String>() {
@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之前的数据)