一、前言
前幾天發布了一篇【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