一、前言
前几天发布了一篇【Jetpack篇】协程+Retrofit网络请求状态封装实战,在评论区里也收到了一些同僚的反馈:
......
具体问题可以直接移步到上一篇评论区查看。
因为有几个问题点还蛮重要,所以就上一篇文章新增了一些内容,主要如下:
✅ 新增局部状态管理。如同一个页面多个接口,可以分别管理状态切换;
✅ UI 层新增 Error,Empty,Success 的 Callback,开发者可以自由选择是否监听,处理业务逻辑更直观、方便;
✅ 结合第三方库 loadSir,统一切换 UI。
✅ 请求调用更加简单
好了,正文开始。
二、局部请求状态管理
很多时候 app 开发,存在同一个界面不同接口的情况,两个接口同时请求,一个成功一个失败,这个时候成功接口继续显示自己的页面,失败接口则显示 Error 提示界面,如下图
上一篇的封装是将 errorLiveData 和 loadingLiveData 全局封装在 BaseFragment 中,而他们的创建也是在 BaseViewModel 中,这样就导致多个接口同时请求时,如果某个接口发送错误,就无法区分错误来自哪里。
如果需要每个接口单独管理自己的状态,那么就需要在 ViewModel 中创建多个 erroeLiveData,这样问题是可以解决,但是会导致代码非常冗余。既然需要每个接口管理不同状态,那就可以新建一个既包含请求返回结果又包含不同状态值的 LiveData,将之命名为 StateLiveData
/**
* MutableLiveData,用于将请求状态分发给UI
*/
class StateLiveData<T> : MutableLiveData<BaseResp<T>>() {
}
复制代码
而 BaseResp 中除了请求返回值的公共 json 外,还需要添加上不同的状态值,我们将状态值分为( STATE_CREATE,STATE_LOADING,STATE_SUCCESS,STATE_COMPLETED,STATE_EMPTY,STATE_FAILED, STATE_ERROR,STATE_UNKNOWN)几种
enum class DataState {
STATE_CREATE,//创建
STATE_LOADING,//加载中
STATE_SUCCESS,//成功
STATE_COMPLETED,//完成
STATE_EMPTY,//数据为null
STATE_FAILED,//接口请求成功但是服务器返回error
STATE_ERROR,//请求失败
STATE_UNKNOWN//未知
}
复制代码
将 DataState 添加到 BaseResp 中,
/**
* json返回的基本类型
*/
class BaseResp<T>{
var errorCode = -1
var errorMsg: String? = null
var data: T? = null
private set
var dataState: DataState? = null
var error: Throwable? = null
val isSuccess: Boolean
get() = errorCode == 0
}
复制代码
那 StateLiveData 该如何使用呢?
我们都知道数据请求会有不同的结果,成功,异常或者数据为 null,那么就可以利用不同的结果,将相应的状态设置在 BaseResp 的 DataState 中。直接进入到数据请求 Repository 层,对上篇异常处理做了改进。
open class BaseRepository {
/**
* repo 请求数据的公共方法,
* 在不同状态下先设置 baseResp.dataState的值,最后将dataState 的状态通知给UI
*/
suspend fun <T : Any> executeResp(
block: suspend () -> BaseResp<T>,
stateLiveData: StateLiveData<T>
) {
var baseResp = BaseResp<T>()
try {
baseResp.dataState = DataState.STATE_LOADING
//开始请求数据
val invoke = block.invoke()
//将结果复制给baseResp
baseResp = invoke
if (baseResp.errorCode == 0) {
//请求成功,判断数据是否为空,
//因为数据有多种类型,需要自己设置类型进行判断
if (baseResp.data == null || baseResp.data is List<*> && (baseResp.data as List<*>).size == 0) {
//TODO: 数据为空,结构变化时需要修改判空条件
baseResp.dataState = DataState.STATE_EMPTY
} else {
//请求成功并且数据为空的情况下,为STATE_SUCCESS
baseResp.dataState = DataState.STATE_SUCCESS
}
} else {
//服务器请求错误
baseResp.dataState = DataState.STATE_FAILED
}
} catch (e: Exception) {
//非后台返回错误,捕获到的异常
baseResp.dataState = DataState.STATE_ERROR
baseResp.error = e
} finally {
stateLiveData.postValue(baseResp)
}
}
}
复制代码
executeResp()
为数据请求的公共方法,该方法传入了两个参数,第一个是将数据请求函数当作参数,第二个就是上面新建的StateLiveData
。
方法一开始就新建了一个 BaseResp<T>()对象,将DataState.STATE_LOADING
状态设置给 BaseResp 的 dataState,接着开始对数据请求进行异常处理(具体可查看上一篇),如果 code=0 表示接口请求成功,否则表示接口请求成功,服务器返回错误。在 code=0 时,对返回数据进行判空处理,因为数据有多种类型,这里需要自己设置类型进行判断,为空就将状态设置为DataState.STATE_EMPTY
,否则为
DataState.STATE_SUCCESS
。如果抛出异常,则将状态设置为DataState.STATE_ERROR
,在请求结束后,利用stateLiveData
将带有状态的 baseResp 分发给 UI。
到这里,请求状态都设置完成,接下来只需要根据不同状态,开始进行界面切换处理。
三、结合 LoadSir 界面切换
LoadSir是一个加载反馈页管理框架,状态页自动切换,具体使用在这里就不描述了,需要的可移步github查看。
LiveData 接收数据变化时,UI 会先注册一个接收事件的观察者,接收到请求的数据后就进行 UI 更新,第二节里将不同状态也添加到了数据中,要想对状态也进行监听的话,就需要对 Observer 进行状态处理。
/**
* LiveData Observer的一个类,
* 主要结合LoadSir,根据BaseResp里面的State分别加载不同的UI,如Loading,Error
* 同时重写onChanged回调,分为onDataChange,onDataEmpty,onError,
* 开发者可以在UI层,每个接口请求时,直接创建IStateObserver,重写相应callback。
*/
abstract class IStateObserver<T>(view: View?) : Observer<BaseResp<T>>, Callback.OnReloadListener {
private var mLoadService: LoadService<Any>? = null
init {
if (view != null) {
mLoadService = LoadSir.getDefault().register(view, this,
Convertor<BaseResp<T>> { t ->
var resultCode: Class<out Callback> = SuccessCallback::class.java
when (t?.dataState) {
//数据刚开始请求,loading
DataState.STATE_CREATE, DataState.STATE_LOADING -> resultCode =
LoadingCallback::class.java
//请求成功
DataState.STATE_SUCCESS -> resultCode = SuccessCallback::class.java
//数据为空
DataState.STATE_EMPTY -> resultCode =
EmptyCallback::class.java
DataState.STATE_FAILED ,DataState.STATE_ERROR -> {
val error: Throwable? = t.error
onError(error)
//可以根据不同的错误类型,设置错误界面时的UI
if (error is HttpException) {
//网络错误
} else if (error is ConnectException) {
//无网络连接
} else if (error is InterruptedIOException) {
//连接超时
} else if (error is JsonParseException
|| error is JSONException
|| error is ParseException
) {
//解析错误
} else {
//未知错误
}
resultCode = ErrorCallback::class.java
}
DataState.STATE_COMPLETED, DataState.STATE_UNKNOWN -> {
}
else -> {
}
}
Log.d(TAG, "resultCode :$resultCode ")
resultCode
})
}
}
override fun onChanged(t: BaseResp<T>) {
Log.d(TAG, "onChanged: ${t.dataState}")
when (t.dataState) {
DataState.STATE_SUCCESS -> {
//请求成功,数据不为null
onDataChange(t.data)
}
DataState.STATE_EMPTY -> {
//数据为空
onDataEmpty()
}
DataState.STATE_FAILED,DataState.STATE_ERROR->{
//请求错误
t.error?.let { onError(it) }
}
else -> { }
}
//加载不同状态界面
Log.d(TAG, "onChanged: mLoadService $mLoadService")
mLoadService?.showWithConvertor(t)
}
/**
* 请求数据且数据不为空
*/
open fun onDataChange(data: T?) {
}
/**
* 请求成功,但数据为空
*/
open fun onDataEmpty() {
}
/**
* 请求错误
*/
open fun onError(e: Throwable?) {
}
}
复制代码
IStateObserver
是Observer
接口的实现类,参数传入了一个 View,而这个 View 就是你所要替换的界面,这也就是同个界面,不同模块显示异常不同的关键所在。因为是结合 Loadsir,首先需要初始化 LoadService,再者通过 dataState 的状态值,设置不同的 Callback,例如 Loading 时,设置为LoadingCallback
,Error 时,设置为ErrorCallback
,Empty 时设置为EmptyCallback
,设置完成后,在 onChanged 回调中统一调用showWithConvertor
,也就是切换界面的操作。
而在 onChange 回调中,同样根据状态值,分别分发onDataChange
,onDataEmpty
,onError
的通知。
到这里,完成了不同状态界面切换和状态通知的分发工作。
四、如何使用
上述基本上将整个流程封装完成,使用起来也相对简便。
Repository 层:
class ProjectRepo() : BaseRepository() {
suspend fun loadProjectTree(stateLiveData: StateLiveData<List<ProjectTree>>) {
executeResp({mService.loadProjectTree()},stateLiveData)
}
}
复制代码
直接就一行代码,executeResp 方法中传入 api 的请求,以及 StateLiveData。
ViewModel 层:
class ProjectViewModel : BaseViewModel() {
val mProjectTreeLiveData = StateLiveData<List<ProjectTree>>()
fun loadProjectTree() {
viewModelScope.launch(Dispatchers.IO) {
mRepo.loadProjectTree(mProjectTreeLiveData)
}
}
复制代码
调用依旧是一行代码,新建了一个StateLiveData
,接着直接在viewModelScope
作用域中调用 Repository 层的网络请求,这里记得将StateLiveData
作为参数传进去。
UI 层:
class ProjectFragment : BaseFragment<FragmentProjectBinding, ProjectViewModel>() {
override fun initData() {
mViewModel?.loadProjectTree()
mViewModel?.mProjectTreeLiveData?.observe(this,
object : IStateObserver<List<ProjectTree>>(mBinding?.rvProjectAll) {
override fun onDataChange(data: List<ProjectTree>?) {
super.onDataChange(data)
Log.d(TAG, "onDataChange: ")
data?.let { mAdapter.setData(it) }
}
override fun onReload(v: View?) {
Log.d(TAG, "onReload: ")
mViewModel?.loadProjectTree()
}
override fun onDataEmpty() {
super.onDataEmpty()
Log.d(TAG, "onDataEmpty: ")
}
override fun onError(e: Throwable?) {
super.onError(e)
showToast(e?.message!!)
Log.d(TAG, "onError: ${e?.printStackTrace()}")
}
})
}
}
复制代码
UI 层利用 ViewModel 的 StateLiveData 注册观察者,与以往不同的是,mViewModel?.mProjectTreeLiveData?.observe()
的第二个参数替换为了 IStateObserver,并且传入了一个 View,而这个 View 代表着的是当请求异常时,你所想替换的 UI 界面,同时,也多了几个回调,
onDataChange:请求成功,数据不为空;
onReload:点击重新请求;
onDataEmpty:数据为空时;
onError:请求失败
开发者可以通过自己的业务需求,自由的选择监听。
我们来看看效果。
五、最后
这次的整合弥补了一些细节问题,更符合 App 开发逻辑,当然每个 App 的业务不同,这就要开发者去定制化一些请求细节,但是协程+Retrofit 网络请求的大致思路就是如此。更多详细的代码可移步至 github
源码: 组件化+Jetpack+kotlin+mvvm
请结合【Jetpack篇】协程+Retrofit网络请求状态封装实战
评论