Android實現(xiàn)優(yōu)雅快速的網(wǎng)絡(luò)請求!

目標

  • 簡單調(diào)用、少寫重復(fù)代碼

  • 不依賴第三方庫(只含Retrofit+Okhttp+協(xié)程

  • 完全不懂協(xié)程也能立馬上手(模板代碼)

用Kotlin的方式寫Kotlin代碼,什么意思呢专筷?對比一下下面2個代碼就知道了:

mViewModel.wxArticleLiveData.observe(this, object : IStateObserver<List<WxArticleBean>>() {

    override fun onSuccess(data: List<WxArticleBean>?) {
    }

    override fun onError() {
    }
})
mViewModel.wxArticleLiveData.observeState(this) {

    onSuccess { data: List<WxArticleBean>? ->
    }

    onError {
    }
}

既然是用Kotlin了死相,就不要用Java的方式寫接口回掉了,DSL表達式不香么扁藕?

提供兩種方式實現(xiàn):

  • 方式一代碼量更少沮峡,網(wǎng)絡(luò)請求自帶Loading,不需要手動調(diào)用Loading

  • 方式二解耦更徹底

兩種方式設(shè)計思路在解耦這一塊存在差異亿柑,看具體需求邢疙,沒有誰好誰差,依照自己的項目望薄,哪個更方便用哪個疟游。

基于官方架構(gòu)的封裝:

一、封裝一

核心思想是:通過一個LiveData 貫穿整個流程痕支,借用網(wǎng)上一張圖:

Activity中的代碼示例

點擊請求網(wǎng)絡(luò)
mViewModel.getArticleData()

設(shè)置監(jiān)聽颁虐,只監(jiān)聽成功的結(jié)果,使用默認異常處理

mViewModel.wxArticleLiveData.observeState(this) {
    onSuccess { data ->
        Log.i("wutao","網(wǎng)絡(luò)請求的結(jié)果是:$data")
    }
}

如果需要單獨處理每一個回調(diào)

這些回調(diào)都是可選的卧须,不需要可不實現(xiàn)

mViewModel.wxArticleLiveData.observeState(this) {
    onSuccess { data ->
        Log.i("wutao","網(wǎng)絡(luò)請求的結(jié)果是:$data")
    }

    onEmpty{
        Log.i("wutao", "返回的數(shù)據(jù)是空另绩,展示空布局")
    }

    onFailed {
        Log.i("wutao", "后臺返回的errorCode: $it")
    }

    onException { e ->
        Log.i("wutao","這是非后臺返回的異常回調(diào)")
    }

    onShowLoading {
         Log.i("wutao","自定義單個請求的Loading")
    }

    onComplete {
        Log.i("wutao","網(wǎng)絡(luò)請求結(jié)束")
    }
}

請求自帶Loading

很多網(wǎng)絡(luò)請求都需要Loading花嘶,不想每次都寫onShowLoading{}方法笋籽,也so easy。

mViewModel.wxArticleLoadingLiveData.observeState(this, this) {
    onSuccess { data ->
  Log.i("wutao","網(wǎng)絡(luò)請求的結(jié)果是:$data")
    }
}

observeState()第二個方法傳入ui的引用就可察绷,這樣單個網(wǎng)絡(luò)請求之前會自動加載Loading干签,成功或者失敗自動取消Loading。

上面代碼都是Activity中拆撼,我們來看下ViewModel中容劳。

ViewModel中代碼示例

class MainViewModel{

    private val repository by lazy { WxArticleRepository() }

    val wxArticleLiveData = StateLiveData<List<WxArticleBean>>()

    fun requestNet() {
        viewModelScope.launch {
            repository.fetchWxArticle(wxArticleLiveData)
        }
    }
}

很簡單喘沿,引入對應(yīng)的數(shù)據(jù)倉庫Repo,然后使用協(xié)程執(zhí)行網(wǎng)絡(luò)請求方法竭贩。來看下Repo中的代碼蚜印。

Repository中代碼示例

class WxArticleRepository : BaseRepository() {

    private val mService by lazy { RetrofitClient.service }

    suspend fun fetchWxArticle(stateLiveData: StateLiveData<List<WxArticleBean>>) {
        executeResp(stateLiveData, mService::getWxArticle)
    }  
}
interface ApiService {

    @GET("wxarticle/chapters/json")
    suspend fun getWxArticle(): BaseResponse<List<WxArticleBean>>
}

獲取一個Retrofit實例,然后調(diào)用ApiService接口方法留量。

封裝一的優(yōu)勢

  • 代碼很簡潔窄赋,不需要手寫線程切換代碼,沒有很多的接口回調(diào)楼熄。

  • 自帶Loading狀態(tài)忆绰,不需要手動啟用Loading和關(guān)閉Loading。

  • 數(shù)據(jù)驅(qū)動ui可岂,以LiveData為載體错敢,將頁面狀態(tài)和網(wǎng)絡(luò)結(jié)果通過在LiveData返回給ui。

項目地址見:

github.com/ldlywt/Fast… (分支名字是:withLoading)

封裝一的不足

*封裝一的核心思想是:一個LiveData貫穿整個網(wǎng)絡(luò)請求鏈缕粹。這是它的優(yōu)勢稚茅,也是它的劣勢。

  • 解耦不徹底平斩,違背了"在應(yīng)用的各個模塊之間設(shè)定明確定義的職責(zé)界限"的思想

  • LiveData監(jiān)聽時亚享,如果需要LoadingBaseActivity都需要實現(xiàn)帶有Loading方法接口绘面。

  • obserState()方法第二個參數(shù)中傳入了UI引用欺税。

  • 不能達到"看方法如其意",如果是剛接觸揭璃,會有很多疑問:為什么需要一個livedata作為方法的參數(shù)魄衅。網(wǎng)絡(luò)請求的返回值去哪了?

  • 封裝一還有一個最大的缺陷:對于是多數(shù)據(jù)源塘辅,封裝一就展示了很不友好的一面。

Repository是做一個數(shù)據(jù)倉庫皆撩,項目中獲取數(shù)據(jù)的方式都在這里同意管理扣墩,網(wǎng)絡(luò)獲取數(shù)據(jù)只是其中一個方式而已。

如果想加一個從數(shù)據(jù)庫或者緩存中獲取數(shù)據(jù)扛吞,封裝一想改都不好改呻惕,如果強制改就破壞了封裝,侵入性很大滥比。

針對封裝一的不足亚脆,優(yōu)化出了封裝二。

二盲泛、封裝二

思路
  • 想要解決上面的不足濒持,不能以LiveData為載體貫穿整個網(wǎng)絡(luò)請求键耕。

  • Observe()方法中去掉ui引用,不要小看一個ui引用柑营,這個引用代表著具體的Activity跟Observe耦合起來了屈雄,并且Activity還要實現(xiàn)IUiView接口。

  • 網(wǎng)絡(luò)請求跟Loading狀態(tài)分開了官套,需要手動控制Loading酒奶。

  • Repository中的方法都有返回值,會返回結(jié)果奶赔,也不需要用livedata作為方法參數(shù)惋嚎。

  • LiveData只存在于ViewModel中,LiveData不會貫穿整個請求鏈站刑。Repository中也不需要LiveData的引用另伍,Repository的代碼就是單純的獲取數(shù)據(jù)。

  • 針對多數(shù)據(jù)源笛钝,也非常好處理质况。

  • 跟ui沒任何關(guān)系,可以完全作為一個獨立的Lib使用玻靡。

Activity中代碼

// 請求網(wǎng)絡(luò)
mViewModel.login("username", "password")

// 注冊監(jiān)聽
mViewModel.userLiveData.observeState(this) {
    onSuccess {data ->
        mBinding.tvContent.text = data.toString()
    }

    onComplete {
        dismissLoading()
    }
}

observeState()中不再需要一個ui引用了结榄。

ViewModel

class MainViewModel {

    val userLiveData = StateLiveData<User?>()

    fun login(username: String, password: String) {
        viewModelScope.launch {
            userLiveData.value = repository.login(username, password)
        }
    }
}

通過livedata的setValue或者postValue方法將數(shù)據(jù)發(fā)送出去。

Repository中

suspend fun login(username: String, password: String): ApiResponse<User?> {
    return executeHttp {
        mService.login(username, password)
    }
}

Repository中的方法都返回請求結(jié)果囤捻,并且方法參數(shù)不需要livedata臼朗。Repository完全可以獨立出來了。

針對多數(shù)據(jù)源
// WxArticleRepository
class WxArticleRepository : BaseRepository() {

    private val mService by lazy {
        RetrofitClient.service
    }

    suspend fun fetchWxArticleFromNet(): ApiResponse<List<WxArticleBean>> {
        return executeHttp {
            mService.getWxArticle()
        }
    }

    suspend fun fetchWxArticleFromDb(): ApiResponse<List<WxArticleBean>> {
        return getWxArticleFromDatabase()
    }
}
// MainViewModel.kt  
private val dbLiveData = StateLiveData<List<WxArticleBean>>()
private val apiLiveData = StateLiveData<List<WxArticleBean>>()
val mediatorLiveDataLiveData = MediatorLiveData<ApiResponse<List<WxArticleBean>>>().apply {
    this.addSource(apiLiveData) {
        this.value = it
    }
    this.addSource(dbLiveData) {
        this.value = it
    }
}

可以看到蝎土,封裝二更符合職責(zé)單一原則视哑,Repository單純的獲取數(shù)據(jù),ViewModel對數(shù)據(jù)進行處理和發(fā)送誊涯。

三挡毅、實現(xiàn)原理

數(shù)據(jù)來源于鴻洋大神的玩Android 開放API

回數(shù)據(jù)結(jié)構(gòu)定義:

{
    "data": ...,
    "errorCode": 0,
    "errorMsg": ""
}

封裝一和封裝二的代碼差距很小,主要看封裝二暴构。

定義數(shù)據(jù)返回類

open class ApiResponse<T>(
        open val data: T? = null,
        open val errorCode: Int? = null,
        open val errorMsg: String? = null,
        open val error: Throwable? = null,
) : Serializable {
    val isSuccess: Boolean
        get() = errorCode == 0
}

data class ApiSuccessResponse<T>(val response: T) : ApiResponse<T>(data = response)

class ApiEmptyResponse<T> : ApiResponse<T>()

data class ApiFailedResponse<T>(override val errorCode: Int?, override val errorMsg: String?) : ApiResponse<T>(errorCode = errorCode, errorMsg = errorMsg)

data class ApiErrorResponse<T>(val throwable: Throwable) : ApiResponse<T>(error = throwable)

基于后臺返回的基類跪呈,根據(jù)不同的結(jié)果,定義不同的狀態(tài)數(shù)據(jù)類取逾。

網(wǎng)絡(luò)請求統(tǒng)一處理:BaseRepository

open class BaseRepository {

    suspend fun <T> executeHttp(block: suspend () -> ApiResponse<T>): ApiResponse<T> {
        runCatching {
            block.invoke()
        }.onSuccess { data: ApiResponse<T> ->
            return handleHttpOk(data)
        }.onFailure { e ->
            return handleHttpError(e)
        }
        return ApiEmptyResponse()
    }

    /**
     * 非后臺返回錯誤耗绿,捕獲到的異常
     */
    private fun <T> handleHttpError(e: Throwable): ApiErrorResponse<T> {
        if (BuildConfig.DEBUG) e.printStackTrace()
        handlingExceptions(e)
        return ApiErrorResponse(e)
    }

    /**
     * 返回200,但是還要判斷isSuccess
     */
    private fun <T> handleHttpOk(data: ApiResponse<T>): ApiResponse<T> {
        return if (data.isSuccess) {
            getHttpSuccessResponse(data)
        } else {
            handlingApiExceptions(data.errorCode, data.errorMsg)
            ApiFailedResponse(data.errorCode, data.errorMsg)
        }
    }

    /**
     * 成功和數(shù)據(jù)為空的處理
     */
    private fun <T> getHttpSuccessResponse(response: ApiResponse<T>): ApiResponse<T> {
        return if (response.data == null || response.data is List<*> && (response.data as List<*>).isEmpty()) {
            ApiEmptyResponse()
        } else {
            ApiSuccessResponse(response.data!!)
        }
    }

}

Retrofit協(xié)程的錯誤碼處理是通過異常拋出來的砾隅,所以通過try...catch來捕捉非200的錯誤碼误阻。包裝成不同的數(shù)據(jù)類對象返回。

擴展LiveData和Observer

在LiveData的Observer()來判斷是哪種數(shù)據(jù)類,進行相應(yīng)的回調(diào)處理:

abstract class IStateObserver<T> : Observer<ApiResponse<T>> {

    override fun onChanged(apiResponse: ApiResponse<T>) {
        when (apiResponse) {
            is ApiSuccessResponse -> onSuccess(apiResponse.response)
            is ApiEmptyResponse -> onDataEmpty()
            is ApiFailedResponse -> onFailed(apiResponse.errorCode, apiResponse.errorMsg)
            is ApiErrorResponse -> onError(apiResponse.throwable)
        }
        onComplete()
    }

再擴展LiveData究反,通過kotlin的DSL表達式替換java的callback回調(diào)寻定,簡寫代碼。

class StateLiveData<T> : MutableLiveData<ApiResponse<T>>() {

    fun observeState(owner: LifecycleOwner, listenerBuilder: ListenerBuilder.() -> Unit) {
        val listener = ListenerBuilder().also(listenerBuilder)
        val value = object : IStateObserver<T>() {

            override fun onSuccess(data: T) {
                listener.mSuccessListenerAction?.invoke(data)
            }

            override fun onError(e: Throwable) {
                listener.mErrorListenerAction?.invoke(e) ?: toast("Http Error")
            }

            override fun onDataEmpty() {
                listener.mEmptyListenerAction?.invoke()
            }

            override fun onComplete() {
                listener.mCompleteListenerAction?.invoke()
            }

            override fun onFailed(errorCode: Int?, errorMsg: String?) {
                listener.mFailedListenerAction?.invoke(errorCode, errorMsg)
            }

        }
        super.observe(owner, value)
    }
}

四奴紧、總結(jié)

封裝一:代碼量更少特姐,可以根據(jù)項目需要封裝一些具體的ui相關(guān),開發(fā)起來更快速黍氮,用起來更爽唐含。

封裝二:解耦更徹底,可以獨立于ui模塊運行沫浆。

個人認為捷枯,框架設(shè)計主要還是服務(wù)于自己的項目需求(開源項目除外),符合設(shè)計模式和設(shè)計原則更好专执,但是不滿足也沒關(guān)系淮捆,適合自己項目需求,能節(jié)省自己的時間本股,就是好的攀痊。

我們自己項目中使用,怎么輕便拄显,怎么快速苟径,怎么寫的爽就怎么來。

本文轉(zhuǎn)自 https://juejin.cn/post/6993294489125126151躬审,如有侵權(quán)棘街,請聯(lián)系刪除。

文末

您的點贊收藏就是對我最大的鼓勵承边! 歡迎關(guān)注我遭殉,分享Android干貨,交流Android技術(shù)博助。 對文章有何見解险污,或者有何技術(shù)問題,歡迎在評論區(qū)一起留言討論富岳!

最后罗心,我整理了一些學(xué)習(xí)資料,里面包括Java基礎(chǔ)城瞎、Android進階、架構(gòu)設(shè)計疾瓮、NDK脖镀、音視頻開發(fā)、跨平臺、底層源碼等技術(shù)蜒灰,還有2022年一線大廠最新面試題集錦弦蹂,都分享給大家,助大家學(xué)習(xí)路上披荊斬棘~ 能力得到提升强窖,思維得到開闊~ 有需要的可以點擊下方公號鏈接免費獲取凸椿。

公眾號鏈接:https://mp.weixin.qq.com/s/ZDvWgZ_huo4natNk26ovlw

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市翅溺,隨后出現(xiàn)的幾起案子脑漫,更是在濱河造成了極大的恐慌,老刑警劉巖咙崎,帶你破解...
    沈念sama閱讀 212,454評論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件优幸,死亡現(xiàn)場離奇詭異,居然都是意外死亡褪猛,警方通過查閱死者的電腦和手機网杆,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來伊滋,“玉大人碳却,你說我怎么就攤上這事⌒ν” “怎么了昼浦?”我有些...
    開封第一講書人閱讀 157,921評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長燥撞。 經(jīng)常有香客問我座柱,道長,這世上最難降的妖魔是什么物舒? 我笑而不...
    開封第一講書人閱讀 56,648評論 1 284
  • 正文 為了忘掉前任色洞,我火速辦了婚禮,結(jié)果婚禮上冠胯,老公的妹妹穿的比我還像新娘火诸。我一直安慰自己,他們只是感情好荠察,可當我...
    茶點故事閱讀 65,770評論 6 386
  • 文/花漫 我一把揭開白布置蜀。 她就那樣靜靜地躺著,像睡著了一般悉盆。 火紅的嫁衣襯著肌膚如雪盯荤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,950評論 1 291
  • 那天焕盟,我揣著相機與錄音秋秤,去河邊找鬼。 笑死,一個胖子當著我的面吹牛灼卢,可吹牛的內(nèi)容都是我干的绍哎。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼鞋真,長吁一口氣:“原來是場噩夢啊……” “哼崇堰!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起涩咖,我...
    開封第一講書人閱讀 37,817評論 0 268
  • 序言:老撾萬榮一對情侶失蹤海诲,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后抠藕,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體饿肺,經(jīng)...
    沈念sama閱讀 44,275評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,592評論 2 327
  • 正文 我和宋清朗相戀三年盾似,在試婚紗的時候發(fā)現(xiàn)自己被綠了敬辣。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,724評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡零院,死狀恐怖溉跃,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情告抄,我是刑警寧澤撰茎,帶...
    沈念sama閱讀 34,409評論 4 333
  • 正文 年R本政府宣布,位于F島的核電站打洼,受9級特大地震影響龄糊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜募疮,卻給世界環(huán)境...
    茶點故事閱讀 40,052評論 3 316
  • 文/蒙蒙 一炫惩、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧阿浓,春花似錦他嚷、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,815評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至退敦,卻和暖如春粘咖,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背侈百。 一陣腳步聲響...
    開封第一講書人閱讀 32,043評論 1 266
  • 我被黑心中介騙來泰國打工涂炎, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留忠聚,地道東北人。 一個月前我還...
    沈念sama閱讀 46,503評論 2 361
  • 正文 我出身青樓唱捣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親网梢。 傳聞我的和親對象是個殘疾皇子震缭,可洞房花燭夜當晚...
    茶點故事閱讀 43,627評論 2 350

推薦閱讀更多精彩內(nèi)容