Fragment重建数据重新请求了?
在上一篇《起初Jetpack Navigation把我逼疯了,可是后来真香》中我提到了,Navigation要结合ViewModel+LiveData使用才能更好的使用好这个Jetpack组件。
很快就有同学问我:
没错!这个问题我当时也遇到了,那么我说说我是怎么做的吧。
为什么会有这样的问题?
为什么会有同学问到:“那接口请求应该放在Fragment的哪个回调方法里”
因为当Navigation返回上一个页面的时候,上一个Fragment直接被重建了,重新执行了onCreateView->onViewCreated这也导致了整个页面的初始化逻辑会重新执行。
但是由于结合了ViewModel+LiveData数据被保存在LiveData里,当执行数据观察observe的时候数据会直接回调,并执行UI更新逻辑。
但是!
这个数据的获取肯定是要有一个执行方法的。我们常规的一些操作很多都是直接放在UI初始化之后请求数据。
override fun initViewAndData(view: View) {
//。。。。。以上忽略初始化UI代码
//执行初始化数据
viewModel.initUserInfo()
}
所以呢,当Fragment页面重建的时候,这个数据请求操作又执行了一遍,这肯定是我们不想要的效果,因为LiveData已经为我们保存了数据,再请求一次数据就是在浪费资源了。
所以才会有这样的提问:接口请求应该放在Fragment的哪个回调方法里?
有什么办法可以防止页面重建时重复请求数据?
当然有办法了。
一种是我们判断LiveData中是否含有数据进行一个if判断
override fun initViewAndData(view: View) {
//。。。。。以上忽略初始化UI代码
if (viewModel.userInfoLiveData.value == null) {
viewModel.initUserInfo()
}
}
我不是很喜欢这种方式,这是我一开始遇到这个重复请求问题时第一时间想到的。我自己都觉得做这种判断真的是太麻烦了,而且如果有的页面我忘记了判断怎么办。所以我下一秒就放弃这个操作了。
还有一种操作就是在ViewModel的构造函数中执行initUserInfo()虽然这样也ok,但如果数据请求失败了怎么办呢?我得知道这个数据是否请求成功,是否请求失败啊。。。。
后来我想到我可观察这个数据改变的状态,数据的有无也是一种数据状态。开始获取数据、数据获取完成也是一种状态。那么我观察数据的这几种状态来判断是否需要进行网络请求不就可以了。
首先就是要对LiveData重新定义,丰富LiveData的操作。
改造LiveData
因为数据已经被我定义成了一种状态,那么我们只能对MutableLiveData下手了。因为MutableLiveData是可变数据,可以修改值,可以获取值。
首先我们要明确我们需要什么状态:
1.一种能判断数据处于初始化的状态 —— RESET 重置
2.数据开始发生改变的状态,这样我可以给个Loading弹窗提示——START 开始
3.数据改变成功的状态,肯定要有一个这样的状态——SUCCESS 成功
4.有成功就有失败——ERROR 错误
5.我还要有一个数据完成的状态,也能标记这个数据已经完成了他的初始化使命——COMPLETE 完成/结束
经过我这么一倒腾我找到了5个标记数据的状态我使用一个枚举列出它们。
enum class LiveDataStatus {
/**
* 数据状态发生改变之前
* 当进行数据处理操作(请求网络,查询数据库...)操作发生之前
*/
START,
/**
* 数据状态改变成功
* 当数据成功获取并赋值成功
* 调用[androidx.lifecycle.MutableLiveData.setValue]后的状态
*/
SUCCESS,
/**
* 数据状态改变失败
* 当数据处理操作(请求网络,查询数据库...)执行失败时,
* 不建议调用 [androidx.lifecycle.MutableLiveData.setValue]
*/
ERROR,
/**
* 完成状态 仅当数据改变成功后由SUCCESS状态改变成COMPLETE
*/
COMPLETE,
/**
* 数据处于初始化状态,数据需要重新进行改变,用于reload数据
*/
RESET;
/**
* 每个状态包含一个消息默认为自身String
*/
var message: String
private set
fun setMessage(message: String): LiveDataStatus {
this.message = message
return this
}
init {
message = toString()
}
}
有注意到我在枚举中增加了一个message变量,为了能够让它携带一些信息设置的。可以设置为Object,当然这个数据也没怎么用到过🤭。
把MutableLiveData增加一个状态标记
为了能够让MutableLiveData拥有这些状态我们要继承MutableLiveData
public class MutableLiveDataStatus<T> extends MutableLiveData<T> {
/**
* 数据状态
* <p>
* 记录数据上一次的状态
*/
private final MutableLiveData<LiveDataStatus> mLiveDataStatus = new MutableLiveData<>();
/**
* 重写构造方法,设置数据状态
*/
public MutableLiveDataStatus() {
super();
//数据默认是需要重设的
setReset();
}
}
(因为最初版我是用Java写的,所以这里就是Java代码了)
然后我们在重写的MutableLiveDataStatus<T>中增加一个MutableLiveData<LiveDataStatus>变量,用于保存我们的数据状态。注意这里的泛型类型是LiveDataStatus是我们定义的枚举类型
再增加几个方法来修改数据状态
/**
* 数据改变前
*/
public void setStart() {
changeStatus(LiveDataStatus.START);
}
/**
* 数据改变出现错误
* @param msg 一些错误描述信息
*/
public void setError(String msg) {
changeStatus(LiveDataStatus.ERROR.setMessage(msg));
}
/**
* 数据需要重置
*/
public void setReset() {
changeStatus(LiveDataStatus.RESET);
}
/**
* 修改状态
* @param liveDataStus 状态类型
*/
private void changeStatus(LiveDataStatus liveDataStatus) {
mLiveDataStatus.setValue(liveDataStatus);
}
这些都是常规的几个操作,需要我们手动去调用并设置这几个状态。
我们在发起网络请求的时候修改数据状态为START,当数据请求失败后设置为ERROR并增加一些描述信息。在有必要重置数据的时候将数据状态修改为RESET。
因为一开始实例化这个MutableLiveDataStatus的时候,在构造函数中就已经给它附加了一个状态RESET,所以我们的数据就有了一个默认状态。
那SUCCESS和COMPLETE我们怎么设置?
我们交给setValue当数据被赋值的时候,自动修改这两个状态。
/**
* 设置有效数据
*
* @param value 数据 我们尽量不要将数据设置为null
*/
@Override
public void setValue(T value) {
setValue(value, LiveDataStatus.SUCCESS.getMessage());
}
/**
* 设置数据,同时设置Success消息信息
*
* @param value 数据
* @param msg 消息
*/
public void setValue(T value, String msg) {
super.setValue(value);
changeStatus(LiveDataStatus.SUCCESS.setMessage(msg));
//状态恢复到完成状态
changeStatus(LiveDataStatus.COMPLETE);
}
有人会问,为什么SUCCESS不放在super.setValue()之前?
这个起初我也是这么想的,我后面会讲。
有postValue可以不用重写,因为postValue最后会调用setValue所以只需要在setValue中做好逻辑处理就行了。
现在我们有了数据状态的标记。就要去使用这个标记搞点事情去。
我们重写LiveData的observe方法并给它几个重载方法。用于我们UI对数据状态的观察。
/**
* 数据状态观测
*
* @param owner 生命周期
* @param observer 数据改变
* @param observerStatus 状态改变
*/
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer, @NonNull Observer<LiveDataStatus> observerStatus) {
this.observe(owner, observer);
observeStatus(owner, observerStatus);
}
/**
* 数据观察
*
* @param owner 生命周期
* @param observer 数据改变
*/
@Override
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer) {
super.observe(owner, observer);
}
/**
* 数据状态观察
*
* @param owner 生命周期
* @param observer 状态改变
*/
public void observeStatus(@NonNull LifecycleOwner owner, @NonNull Observer<LiveDataStatus> observer) {
mLiveDataStatus.observe(owner, observer);
}
/**
* 在fragment中要使用ViewLifecycleOwner
*/
public void observe(@NonNull Fragment fragment, @NonNull Observer<? super T> observer, @NonNull Observer<LiveDataStatus> observerStatus) {
this.observe(fragment.getViewLifecycleOwner(), observer, observerStatus);
}
/**
* 在fragment中要使用ViewLifecycleOwner
*/
public void observe(@NonNull Fragment fragment, @NonNull Observer<? super T> observer) {
this.observe(fragment.getViewLifecycleOwner(), observer);
}
/**
* 在fragment中要使用ViewLifecycleOwner
*/
public void observeStatus(@NonNull Fragment fragment, @NonNull Observer<LiveDataStatus> observer) {
this.observeStatus(fragment.getViewLifecycleOwner(), observer);
}
那么我们其实已经将整个LiveData继承封装好了,现在就去用它。其实这个observe**的方法封装的有些多了。如果是kotlin会封装的更简单一些。
我们可以分别观察数据的改变,和数据状态的改变。这样是ok的。之后我会把源码整出来。
viewModel.userInfoLiveData.observe(this) {
Log.d(TAG, "initLiveData: onChange")
adapter.setNewInstance(it)
}
viewModel.userInfoLiveData.observeStatus(this) {
when (it) {
START -> Log.d(TAG, "数据开始请求")
SUCCESS -> {
Log.d(TAG, "数据请求 成功")
Toast.makeText(requireContext(), "数据请求 成功", Toast.LENGTH_SHORT).show()
}
ERROR -> {
Log.d(TAG, "数据请求 失败")
Toast.makeText(requireContext(), "数据请求失败", Toast.LENGTH_SHORT).show()
}
COMPLETE -> Log.d(TAG, "数据请求 完成")
RESET -> {
Log.d(TAG, "需要重置数据")
viewModel.initUserInfo()
}
}
}
当然我在封装MutableLiveDataStatus的时候还给了一个方法:
/**
* 数据状态观测
*
* @param owner 生命周期
* @param observer 数据改变
* @param observerStatus 状态改变
*/
public void observe(@NonNull LifecycleOwner owner, @NonNull Observer<? super T> observer, @NonNull Observer<LiveDataStatus> observerStatus) {
this.observe(owner, observer);
observeStatus(owner, observerStatus);
}
我们还可以这么用:
viewModel.userInfoLiveData.observe(this,{
adapter.setNewInstance(it)
},{
when (it) {
START -> Log.d(TAG, "数据开始请求")
SUCCESS -> {
Log.d(TAG, "数据请求 成功")
Toast.makeText(requireContext(), "数据请求 成功", Toast.LENGTH_SHORT).show()
}
ERROR -> {
Log.d(TAG, "数据请求 失败")
Toast.makeText(requireContext(), "数据请求失败", Toast.LENGTH_SHORT).show()
}
COMPLETE -> Log.d(TAG, "数据请求 完成")
RESET -> {
Log.d(TAG, "需要重置数据")
viewModel.initUserInfo()
}
}
})
还记得一开始创建MutableLiveDataStatus对象的时候,我给了一个默认的RESET状态吗。那么现在我们用到了。
当我们起初第一次观察数据的时候,数据处于RESET初始状态,我们就可以根据这个状态来判断页面是否需要初始化数据,然后去执行我们的请求数据操作viewModel.initUserInfo()。当数据请求完成并将数据状态设置成COMPLETE那么整个流流程就走完了。
整个MutableLiveDataStatus的状态流程差不是这样,大家心领神会一下:
结合上面的图我们看一下请求流程👇👇
/**
* 初始化获取用户列表信息
*/
fun initUserInfo() {
//我们在获取数据前修改LiveData的状态,告诉UI准备获取数据了
userInfoLiveData.setStart()
//我这里为了方便查看效果使用了简陋的方式获取数据,可以采用Flow RxJava,整体流程都一样。
testModel.getUserData(canSuccess, object : DataCallBack<MutableList<UserInfo>> {
override fun success(data: MutableList<UserInfo>) {
//当数据能够成功获取到,我们拿到的数据可能不是完全的符合UI展示所以需要在ViewModel中对数据进行处理;
//这里就是做了一个筛选操作。
//最终我们将数据交给LiveData,并由LiveData通知UI更新数据
userInfoLiveData.value = data.filter {
it.userName != "未知" && it.userSex != "未知"
}.toMutableList()
//setValue会使status由START->SUCCESS->COMPLETE
//在SUCCESS的时候如果有加载弹窗,可以在这个窗台触发时关闭。
//页面重建再次观察数据的时候,状态已经是最终的COMPLETE了,
//所以不会触发SUCCESS的代码执行
}
override fun failure(message: String?) {
//当然我们也可以清空数据 不是很建议这么做,减少空指针吧
// userInfoLiveData.value = null
userInfoLiveData.setError(message)
//当数据获取失败的时候状态是ERROR,我们有必要在ERROR的时候做出一些补救措施。
//给一个弹窗提示用户重新获取一下数据什么的。
}
})
//这里是模拟了是否需要请求成功
canSuccess = !canSuccess
}
这样我们不仅拿到了数据,还能知道数据当前处于什么状态,根据状态我们再更新UI显示不同的提示信息。
当页面重建的时候,我们再对数据进行观察的时候,数据的状态处是COMPLETE我们可以对这个状态不做任何的处理,只是知道已经完成就可以了。这样就不会影响SUCCESS状态下的操作。当然你也可以整一个ERROR_COMPLETE状态,标记已经处理错误信息了。
为什么SUCCESS状态要在setValue之后设置?
public void setValue(T value, String msg) {
super.setValue(value);
changeStatus(LiveDataStatus.SUCCESS.setMessage(msg));
//状态恢复到完成状态
changeStatus(LiveDataStatus.COMPLETE);
}
有时候如果你不想观察数据的值,只想知道数据改变的状态,你可以直接在SUCCESS状态触发的时候,直接去MutableLiveDataStatus中获取具体的数据。因为已经是成功状态了,这个数据不可能为NULL。
viewModel.userInfoLiveData.observeStatus(this) { status->
when (status) {
START -> showLoadingDialog()
SUCCESS -> {
hideLoadingDialog()
Log.d(TAG, "数据请求 成功")
val value = viewModel.userInfoLiveData.value
showSuccessDialog()
}
ERROR ->{
hideLoadingDialog()
Log.d(TAG, "数据请求 错误")
showErrorDialog()
}
RESET -> {
Log.d(TAG, "数据是初始的重置状态,需要请求数据")
viewModel.initUserInfo()
}
}
}
这样逻辑看起来也比较的清晰。
我为什么要这么封装
我这样封装其实也是看到一些开源项目中,对这个数据的观察也是在ViewModel中单独写了一个LiveData<Boolean>去判断这个数据状态。感觉这样会让整个ViewModel文件变得臃肿。
而且这种方式,我已经在自己的项目中应用了,暂时没有任何问题。
如果大家有任何问题,请及时与我交流,讨论经验。 一方面,让我知道自己的不足,另一方面,能更好的积累自己的开发经验。
这是我的项目演示地址。 github.com/Dboy233/Nav…
这里是很久之后补充的:
后来我又翻阅了很多LiveData的文章。发现在很久以前就有志同道合的倔友做过这样的封装了。没想到思路竟然异常的相似。还是我之前的功课没做好,不过之后我又自己单独写了一个封装好的代码库。已经提交到了GitHub,全部用kotlin写的封装的更简洁了一些。