利用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中自由預(yù)處理一些邏輯,可以看到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ò)請求

首先感謝該作者,正是由于這篇文章我自己才有了從頭到尾動手走一遍的想法,才有了本文,感謝?。?/p>

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

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容