【Jetpack篇】協程+Retrofit網絡請求狀態封裝實戰(2)

一、前言

前幾天發布了一篇【Jetpack篇】協程+Retrofit網絡請求狀態封裝實戰,在評論區里也收到了一些同僚的反饋:

image.png
image.png

......

具體問題可以直接移步到上一篇評論區查看。

因為有幾個問題點還蠻重要,所以就上一篇文章新增了一些內容,主要如下:

  • ? 新增局部狀態管理。如同一個頁面多個接口,可以分別管理狀態切換;
  • ? UI層新增Error,Empty,Success的Callback,開發者可以自由選擇是否監聽,處理業務邏輯更直觀、方便;
  • ? 結合第三方庫loadSir,統一切換UI。
  • ? 請求調用更加簡單

好了,正文開始。

二、局部請求狀態管理

很多時候app開發,存在同一個界面不同接口的情況,兩個接口同時請求,一個成功一個失敗,這個時候成功接口繼續顯示自己的頁面,失敗接口則顯示Error提示界面,如下圖

image.png

上一篇的封裝是將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?) {

    }
}

IStateObserverObserver接口的實現類,參數傳入了一個View,而這個View就是你所要替換的界面,這也就是同個界面,不同模塊顯示異常不同的關鍵所在。因為是結合Loadsir,首先需要初始化LoadService,再者通過dataState的狀態值,設置不同的Callback,例如Loading時,設置為LoadingCallback,Error時,設置為ErrorCallback,Empty時設置為EmptyCallback,設置完成后,在onChanged回調中統一調用showWithConvertor,也就是切換界面的操作。

而在onChange回調中,同樣根據狀態值,分別分發onDataChangeonDataEmptyonError的通知。

到這里,完成了不同狀態界面切換和狀態通知的分發工作。

四、如何使用

上述基本上將整個流程封裝完成,使用起來也相對簡便。

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:請求失敗

開發者可以通過自己的業務需求,自由的選擇監聽。

我們來看看效果。

80cc44c18c36363d3d8d589814397a48.gif

五、最后

這次的整合彌補了一些細節問題,更符合App開發邏輯,當然每個App的業務不同,這就要開發者去定制化一些請求細節,但是協程+Retrofit網絡請求的大致思路就是如此。
更多詳細的代碼可移步至github

源碼: 組件化+Jetpack+kotlin+mvvm

請結合【Jetpack篇】協程+Retrofit網絡請求狀態封裝實戰

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,818評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,185評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,656評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,647評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,446評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,951評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,041評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,189評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,718評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,800評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,419評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,420評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,755評論 2 371

推薦閱讀更多精彩內容