利用Kotlin和協(xié)程實現(xiàn)DSL樣式的網(wǎng)絡(luò)請求

利用Kotlin和協(xié)程實現(xiàn)DSL樣式的網(wǎng)絡(luò)請求

本文將基于retrofit2.62公般、okhttp4.0万搔、Coroutines、viewModel-ktx官帘、LiveData-ktx力求實現(xiàn)一種分層清晰瞬雹、整潔靈活、處理方便的網(wǎng)絡(luò)請求刽虹。

技術(shù)棧

為了擁抱Kotlin酗捌,okHttp已經(jīng)將okhttp全部用Kotlin重寫。同時okHttp的老朋友retrofit也擁抱了Coroutines推出了retrofit2.60涌哲。

DSL方式的語法特性or代碼樣式在各個開源庫中也露臉越來越多胖缤。比如剛剛官宣停止維護的Anko和如日中天的Flutter。關(guān)于DSL的更多介紹本文最后將給出學習鏈接膛虫。DSL的書寫風格在靈活配置請求和處理請求上給人耳目一新草姻、整潔靈活、清晰可讀的觀感稍刀。

本文所實現(xiàn)網(wǎng)絡(luò)請求的特點

DSL方式的請求撩独,自由處理各種start、response账月、error回調(diào),或者交給BaseViewModel統(tǒng)一處理

回調(diào)方式請求综膀,自由處理各種start、response局齿、error回調(diào),或者交給BaseViewModel統(tǒng)一處理

LiveData方式請求,請求直接返回LiveData

DSL方式靈活配置OkHttpClient/Retrofit

ShowCode

請求的聲明如下

interface TestService {
    @GET("/banner/json")
    suspend fun getBanner(): WanResponse<List<Banner>>
}

可以看到加成了協(xié)程以后的retrofit在生命網(wǎng)絡(luò)請求以后變得異常簡單剧劝,不需要用Call或者Observable進行包裝,直接返回想要的實體類就好抓歼。suspend是Kotlin的關(guān)鍵字讥此,修飾方法是表示為掛起函數(shù),只能運行在協(xié)程或者其他掛起函數(shù)中谣妻。

請求的OKHttp萄喳、Retrofit的配置示例如下

 Request.init(context = this.applicationContext, baseUrl = "https://www.wanandroid.com") {
            okHttp {okhttpBuilder->
                //配置okhttp
                okhttpBuilder
            }

            retrofit {retrofitBuilder->
                //配置retrofit
                retrofitBuilder
            }
        }

如示例代碼所示,通過DSL化的代碼書寫方式可以靈活的通過okHttp或者retrofit代碼塊來靈活配置okHttp和retrofit蹋半。當然用戶可以直接選擇不進行任何配置他巨,基本的配置在Request.kt中已經(jīng)配置完成,在使用默認配置的情況下完全可以不書寫okHttp或者retrofit代碼塊。需要說明的是初始化過程中傳入了context染突,是由于在Request.kt中存在關(guān)于持久化cookie的配置捻爷,cookie持久化到SP中時需要context來創(chuàng)建SP。還有okHttp和retrofit看似是代碼塊其實是帶函數(shù)類型參數(shù)的方法而已份企,正是利用了kotlin對高階函數(shù)也榄、擴展函數(shù)、lambda表達式的友好支持和invoke約定才能寫出如上所示的DSL化的保證可讀性的整潔靈活的代碼薪棒。

關(guān)于DSL方式請求調(diào)用示例如下

class TestViewModel : BaseViewModel() {
    private val service by lazy { Request.apiService(TestService::class.java) }
    val liveData = MutableLiveData<WanResponse<List<Banner>>>()
    fun loadDSL() {
        apiDSL<WanResponse<List<Banner>>> {
            onRequest {
                service.getBanner()
            }
            onResponse {response->
                Log.e("Thread-->onResponse", Thread.currentThread().name)
                Log.e("onResponse-->", Gson().toJson(response))
                liveData.value = response
            }
            onStart {
                Log.e("Thread-->onStart", Thread.currentThread().name)
                false
            }
            onError {
                it.printStackTrace()
                Log.e("Thread-->onError", Thread.currentThread().name)
                true
            }
        }
    }
}

如上可見手蝎,在onRequest中一股腦塞入請求就可以在onResponse中拿到請求結(jié)果榕莺。同時也可以在主線程的onStart中自由預處理一些邏輯俐芯,可以看到onStart代碼塊最后默認返回了false,false表示不攔截BaseViewModel中對網(wǎng)絡(luò)請求開始時的處理(比如彈出統(tǒng)一樣式的loading)钉鸯。如果返回true則表示該行為完全由自己處理吧史。同理針對onError也是一樣的道理,可以自己處理錯誤也可以交給base處理唠雕。當然也可以不寫onStart和onError完全交給base來處理相關(guān)行為贸营,使網(wǎng)絡(luò)請求代碼更簡潔。

關(guān)于回調(diào)方式請求調(diào)用示例如下

fun loadCallback() {
    apiCallback({
        service.getBanner()
    }, {
        liveData.value = it//這里是onResponse的回調(diào)
    }, {
        true//這里是onStart的回調(diào)
    },  onError ={ exception ->
        false
    })
}

借助函數(shù)類型(Any) -> Any來定義請求的不同回調(diào)岩睁,比如error的回調(diào)可以定義為((Exception) -> Boolean)?钞脂。接受exception來處理異常,返回bool類型來決定是否繼續(xù)交給base來繼續(xù)處理捕儒。同時定義成可空類型可以默認交給base出路冰啃。但是顯而易見的是這種代碼書寫方式并不如DSL方式的請求美觀和可讀性高。

關(guān)于直接返回LiveData的請求調(diào)用示例如下

fun loadLiveData(): LiveData<Result<WanResponse<List<Banner>>>> {
        return apiLiveData(SupervisorJob() + Dispatchers.Main.immediate, timeoutInMs = 2000) {
            service.getBanner()
        }
 }

在V層拿到LiveData后的操作如下

viewModel.loadLiveData().observe(this, Observer {
                when (it) {
                    is Result.Error -> {
                        hideLoading()
                    }
                    is Result.Response -> {
                        hideLoading()
                        it.response.apply {
                            showToast(Gson().toJson(this))
                        }
                    }
                    is Result.Start -> {
                        showLoading()
                    }
                    else ->{//冗余
                    }
                }
})

顯然這種方式的請求更適合輕量化的請求刘莹,適合拿到結(jié)果直接去渲染view不經(jīng)過二次數(shù)據(jù)處理的場景阎毅。因為如上圖所示在V層處理start、error回調(diào)感覺不是很友好点弯,扇调,在reponse中隱藏loading也是比較繁瑣。但好處是V層直接可以拿到包含請求數(shù)據(jù)的LiveData抢肛,操作更加便捷狼钮。

關(guān)于Livedata的封裝如下

protected fun <Response> apiLiveData(
    context: CoroutineContext = EmptyCoroutineContext,
    timeoutInMs: Long = 3000L,
    request: suspend () -> Response
    ): LiveData<Result<Response>> {

    return androidx.lifecycle.liveData(context, timeoutInMs) {
        emit(Result.Start())
        try {
            emit(withContext(Dispatchers.IO) {
                Result.Response(request())
            })
        } catch (e: Exception) {
            e.printStackTrace()
            emit(Result.Error(e))
        } finally {
            emit(Result.Finally())
        }
    }
}

此處的livedata是lifecycle-livedata-ktx,在配置了timeoutInMs后如果沒有活躍的observers就會超時自動取消捡絮。在IO線程拿到請求的結(jié)果后包裝成Result熬芜,像RxJava那樣發(fā)射出來即可。為了保證返回的livedata中數(shù)據(jù)的一致性锦援,start猛蔽、error也被包裝成了Result。

DSL封裝示例

接下來我們以對okhttp和retrofit的請求配置來看下是怎么進行DSL封裝的,不多說showcode曼库。

class RequestDsl {
    internal var buidOkHttp: ((OkHttpClient.Builder) -> OkHttpClient.Builder)? = null
    internal var buidRetrofit: ((Retrofit.Builder) -> Retrofit.Builder)? = null
    fun okHttp(builder: ((OkHttpClient.Builder) -> OkHttpClient.Builder)?) {
        this.buidOkHttp = builder
    }
    fun retrofit(builder: ((Retrofit.Builder) -> Retrofit.Builder)?) {
        this.buidRetrofit = builder
    }
}

首先是DSL的配置類区岗,主要有2個角色,一個是函數(shù)類型的buidOkHttp毁枯,一個是以buidOkHttp為參數(shù)的配置buidOkHttp的高階函數(shù)okHttp慈缔。可見buidOkHttp變量是一個可空類型的輸入和返回是非空的OkHttpClient.Builder類型的函數(shù)种玛,既然是可空類型的我們在初始化調(diào)用時就可以選擇配置OkHttpClient.Builder與否藐鹤。既然輸入返回都是OkHttpClient.Builder我們就可以拿到既定的帶有初始化配置的OkHttpClient.Builder進行進一部配置,只要最后返回OkHttpClient.Builder就好赂韵,同時OkHttpClient.Builder采用了建造者模式我們可以拿到builder引用之后進行二次配置最后原樣返回builder的引用娱节。

下面是初始化方法的具體實現(xiàn)

private fun initRequest(okHttpBuilder: OkHttpClient.Builder, requestDSL: (RequestDsl.() -> Unit)? = null) {
    val dsl = if (requestDSL != null) RequestDsl().apply(requestDSL) else null
    val finalOkHttpBuilder = dsl?.buidOkHttp?.invoke(okHttpBuilder) ?: okHttpBuilder
    val retrofitBuilder = Retrofit.Builder()
        .baseUrl(this.baseUrl)
        .addConverterFactory(GsonConverterFactory.create())
        .client(finalOkHttpBuilder.build())
    val finalRetrofitBuilder = dsl?.buidRetrofit?.invoke(retrofitBuilder) ?: retrofitBuilder
    this.retrofit = finalRetrofitBuilder.build()
}

這個方法就比較簡單,requestDSL定義為可空類型祭示,可以選擇配置或者不進行額外配置肄满。

此時我們再看一下比較常用的apply方法的如下定義,我們在apply方法中就進入到了泛型T的內(nèi)部空間质涛,this關(guān)鍵字就指代的是泛型自己 稠歉,可以在內(nèi)部調(diào)用泛型的成員。

public inline fun <T> T.apply(block: T.() -> Unit): T

相似的requestDSL也是和apply方法中的block是一樣的類型汇陆。一旦選擇了進行配置就可以像apply方法一樣怒炸,在RequestDsl函數(shù)內(nèi)部選擇性的調(diào)用okHttp或者retrofit方法。那么在關(guān)于DSL方式請求調(diào)用也和配置請求一樣如出一轍不再多說毡代。

協(xié)程的使用

internal fun launch(viewModelScope: CoroutineScope) {
    viewModelScope.launch(context = Dispatchers.Main) {
        onStart?.invoke()
        try {
            val response = withContext(Dispatchers.IO) {
                request()
            }
            onResponse?.invoke(response)
        } catch (e: Exception) {
            e.printStackTrace()
            onError?.invoke(e)
        } finally {
            onFinally?.invoke()
        }
    }
}

整個項目中關(guān)于協(xié)程的使用就只有這一個方法阅羹,其中viewModelScope可以是在viewmodel-ktx中定義的協(xié)程作用域,來避免我們書寫重復的代碼月趟。同在ViewModel.onCleared()被調(diào)用的時候灯蝴,viewModelScope會自動取消作用域內(nèi)的所有協(xié)程。在執(zhí)行請求任務(wù)request()時會切換到IO線程執(zhí)行孝宗,拿到結(jié)果后通過onResponse告訴上層代碼穷躁。

最后關(guān)于base中的統(tǒng)一處理回調(diào)的示例

如下代碼都是在BaseViewModel中定義的

protected fun <Response> apiDSL(apiDSL: ViewModelDsl<Response>.() -> Unit) {
    api<Response> {
        onRequest {
            ViewModelDsl<Response>().apply(apiDSL).request()
        }
        onResponse {
            ViewModelDsl<Response>().apply(apiDSL).onResponse?.invoke(it)
        }
        onStart {
            val override = ViewModelDsl<Response>().apply(apiDSL).onStart?.invoke()
            if (override == null || !override) {
                onApiStart()
            }
            override
        }
        onError { error ->
            val override = ViewModelDsl<Response>().apply(apiDSL).onError?.invoke(error)
            if (override == null || !override) {
                onApiError(error)
            }
            override
        }
    }
}

我們重點關(guān)注api請求發(fā)起時start、出錯時error的處理因妇,其中涉及的onApiStart()和onApiFinally()的定義如下

protected open fun onApiStart() {
    apiLoading.value = true//apiLoading: MutableLiveData<Boolean>
}
protected open fun onApiError(e: Exception?) {
    apiLoading.value = false
    apiException.value = e//apiException: MutableLiveData<Throwable>
}

在方法apiDSL中進行了一次DSL的嵌套问潭,apiDSL是業(yè)務(wù)代碼配置的代碼。如果apiDSL沒有配置onStart或者最后返回了false婚被,那么表示還需要base進一步處理start的回調(diào).此時就會調(diào)用base中定義的onApiStart()更新loading的liveData狡忙,然后再V層中拿到apiLoading統(tǒng)一彈出或關(guān)閉loadingDialog。

代碼地址

https://github.com/RunFeifei/Run

感謝

像使用gradle一樣址芯,在kotlin中進行網(wǎng)絡(luò)請求

首先感謝該作者灾茁,正是由于這篇文章我自己才有了從頭到尾動手走一遍的想法窜觉,才有了本文,感謝1弊ā禀挫!

Kotlin DSL原理解析:帶接收者的lambda以及invoke約定

感謝該作者,這篇文檔真正讓我開始漸漸熟悉DSL拓颓,慢慢理會DSL

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末语婴,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子驶睦,更是在濱河造成了極大的恐慌砰左,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,941評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件场航,死亡現(xiàn)場離奇詭異缠导,居然都是意外死亡,警方通過查閱死者的電腦和手機旗闽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,397評論 3 395
  • 文/潘曉璐 我一進店門酬核,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人适室,你說我怎么就攤上這事【俟澹” “怎么了捣辆?”我有些...
    開封第一講書人閱讀 165,345評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長此迅。 經(jīng)常有香客問我汽畴,道長,這世上最難降的妖魔是什么耸序? 我笑而不...
    開封第一講書人閱讀 58,851評論 1 295
  • 正文 為了忘掉前任忍些,我火速辦了婚禮,結(jié)果婚禮上坎怪,老公的妹妹穿的比我還像新娘罢坝。我一直安慰自己,他們只是感情好搅窿,可當我...
    茶點故事閱讀 67,868評論 6 392
  • 文/花漫 我一把揭開白布嘁酿。 她就那樣靜靜地躺著,像睡著了一般男应。 火紅的嫁衣襯著肌膚如雪闹司。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,688評論 1 305
  • 那天沐飘,我揣著相機與錄音游桩,去河邊找鬼牲迫。 笑死,一個胖子當著我的面吹牛借卧,可吹牛的內(nèi)容都是我干的恩溅。 我是一名探鬼主播,決...
    沈念sama閱讀 40,414評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼谓娃,長吁一口氣:“原來是場噩夢啊……” “哼脚乡!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起滨达,我...
    開封第一講書人閱讀 39,319評論 0 276
  • 序言:老撾萬榮一對情侶失蹤奶稠,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后捡遍,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锌订,經(jīng)...
    沈念sama閱讀 45,775評論 1 315
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年画株,在試婚紗的時候發(fā)現(xiàn)自己被綠了辆飘。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,096評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡谓传,死狀恐怖蜈项,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情续挟,我是刑警寧澤紧卒,帶...
    沈念sama閱讀 35,789評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站诗祸,受9級特大地震影響跑芳,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜直颅,卻給世界環(huán)境...
    茶點故事閱讀 41,437評論 3 331
  • 文/蒙蒙 一博个、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧功偿,春花似錦盆佣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,993評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至养葵,卻和暖如春征堪,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背关拒。 一陣腳步聲響...
    開封第一講書人閱讀 33,107評論 1 271
  • 我被黑心中介騙來泰國打工佃蚜, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留庸娱,地道東北人。 一個月前我還...
    沈念sama閱讀 48,308評論 3 372
  • 正文 我出身青樓谐算,卻偏偏與公主長得像熟尉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子洲脂,可洞房花燭夜當晚...
    茶點故事閱讀 45,037評論 2 355