利用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