還有比Retrofit更簡單易用的網(wǎng)絡(luò)請求方案嗎?

還有比Retrofit更簡單易用的網(wǎng)絡(luò)請求方案嗎?

版權(quán)聲明:本文為博主原創(chuàng)文章,轉(zhuǎn)載請附上原文出處鏈接和本聲明。
鏈接:giswangsj

B站視頻教程:【Android開發(fā)教程】Android開發(fā)——網(wǎng)絡(luò)訪問框架源碼解析合集

前言

Retrofit是當前業(yè)內(nèi)非常流行的網(wǎng)絡(luò)請求框架,它簡單易用,幾乎是Android開發(fā)必備。同時它使用到了大量的設(shè)計模式,其代碼值得我們仔細研讀,其設(shè)計思想值得我們深入思考。。。

為了防止大佬們噴我,先吹一波Retrofit,接下來探討一下在Kotlin+協(xié)程環(huán)境下比Retrofit更簡單易用的封裝方式。

首先提出兩個問題:

  1. 有必要對網(wǎng)絡(luò)請求做一層包裝嗎?

    個人感覺很有必要,就拿Retrofit來說對業(yè)務(wù)代碼的侵入是比較大的,從長遠的角度來考慮,沒有任何框架敢說自己是yyds,一旦有一天出了新技術(shù)想要換框架,面對那么多的業(yè)務(wù)代碼不由得腦海中就會有一萬頭羊駝奔騰而過。

  2. 如果做包裝層,那Retrofit還有使用的必要嗎?

    首先為什么要使用Retrofit?它相較于OkHttp主要做了三件事情:1,線程切換。2,數(shù)據(jù)解析。3,請求參數(shù)可配置化。

    對于線程切換我們用Kt協(xié)程可以很容易實現(xiàn)。對于數(shù)據(jù)解析如果能夠拿到返回結(jié)果的Type,也就是一行代碼的事情。對于請求參數(shù),通過簡單的封裝也可以很方便設(shè)置。

因此本文的目的就是扔掉Retrofit,借助Kt協(xié)程對OkHttp做一層包裝。

先看一下最終使用效果:

viewModelScope.launch(Dispatchers.IO) { 
    // 感謝玩安卓提供的api
    val url = "https://wanandroid.com/wxarticle/chapters/json"
    // 請求網(wǎng)絡(luò)返回數(shù)據(jù)
    val result = HttpUtils.get<List<Chapters>>(url)
}
復(fù)制代碼

明確目的

對于一個網(wǎng)絡(luò)請求工具我們想要的無非就是我們把參數(shù)傳進去,然后把期望的結(jié)果返回來。

此時心中應(yīng)該大概有了想要的效果:

HttpUtils.get(url, param, header, object: Callback<T>{ 
    onSuccess{}
    onError{}
})

HttpUtils.post(url, body, header, object: Callback<T>{ 
    onSuccess{}
    onError{}
})
復(fù)制代碼

要傳的參數(shù)包括url,Query參數(shù),Body,Header等,另外為了使其返回數(shù)據(jù)解析后的結(jié)果,還需要傳結(jié)果類型的Type。其他參數(shù)都好說,唯獨返回結(jié)果的Type該怎么傳遞呢?接下來的兩個方案都是圍繞這個問題展開的。

方案一

首先定義一下返回結(jié)果:

data class ChaptersResp() {
    var data = arrayListOf<Chapters>(),
    var errorCode: Int,
    var errorMsg: String
}

data class Chapters(
    var courseId: String,
    var id: Int,
    var name: String,
    var order: Int
)
復(fù)制代碼

前面已經(jīng)提到,重點在于如何傳遞期望返回類型的Type。

如果是對象如ChaptersResp,我們可以通過ChaptersResp.class作為參數(shù)傳遞,但是項目中一般都會有一個BaseResp,因此只需要定義一個Chapters就可以了,那問題來了,如何傳遞List<Chapters>呢?總不能傳個(List<Chapters>).class吧。

1,Object.class方式

如果非要通過這種Object.class方式傳遞,有兩種方式:1,傳遞完整對象的class,如:ChaptersResp.class。2,從方法上解決,將單體對象和集合對象分開,如返回結(jié)果是單體對象就用HttpUtils.get(),集合對象就用HttpUtils.getList(),然后在方法內(nèi)部進行區(qū)分。如下:

// 1,傳參上解決:傳整體對象
HttpUtils.get(url, param, ChaptersResp.class, object: HttpCallback<ChaptersResp> {
    onSuccess{}
    onError{}
})

// 2,從方法上解決:傳遞Chapters的class,然后在getList方法中解析成List
HttpUtils.getList(url, param, Chapters.class, object: HttpCallback<List<Chapters>> {
    onSuccess{}
    onError{}
})
復(fù)制代碼

這應(yīng)該是最low的方式了,只有新手才會這么搞吧,所有一般不會這么搞。

從上面的方法可以看出在回調(diào)中通過泛型已經(jīng)添加了期望返回的類型。那能否從其中獲取呢?

2,泛型中獲取

在泛型類中可以通過getGenericSuperclass() 獲取當前類表示的實體(類,接口,基本類型或void)的直接父類的Type,通過getActualTypeArguments()可以獲取參數(shù)數(shù)組。如下:

public abstract class HttpCallback<T> implements CallBack<T> {

    @Override
    public void onNext(String data) {
        // 獲取GenericSuperclass
        Type type = getClass().getGenericSuperclass();
        if (type instanceof ParameterizedType) {
            // 獲取泛型的Type數(shù)組
            Type[] types = ((ParameterizedType) type).getActualTypeArguments();
            // 由于這里是有一個泛型T,因此取第一個就是傳進來的泛型的Type
            T result = new Gson().fromJson(data, types[0]);
            onSuccess(result, "" + data.getCode(), data.getMsg());
        } else {
            throw new ClassCastException();
        }
    }
}
復(fù)制代碼

在拿到請求結(jié)果以后通過回調(diào)的onNext()將返回結(jié)果交給回調(diào)來處理。在onNext()中解析完數(shù)據(jù)再調(diào)用onSuccess()等其他回調(diào)。

這么一來請求就可以這么寫:

HttpUtils.get(url, param, object: HttpCallback<List<Chapters>> {
    onSuccess{}
    onError{}
})
復(fù)制代碼

小結(jié):該方案使用回調(diào)的方式,通過回調(diào)這個匿名內(nèi)部獲取返回類型的Type,最終把解析后的結(jié)果回調(diào)出來。

方案二

由于這里使用到了Kt協(xié)程,Kt協(xié)程允許我們以同步的方式實現(xiàn)異步的效果,這似乎跟回調(diào)有點不搭,那如何像Retrofit一樣直接返回結(jié)果呢?

由于沒有了回調(diào)對象,此時面臨的問題依然是返回結(jié)果的類型的傳遞。

Retrofit是在接口方法上配置的返回類型,在動態(tài)代理中通過調(diào)用method的getGenericReturnType()可以獲取到方法返回類型的Type,進而可以獲取到期望返回類型的Type。

// 獲取到Call<List<Chapters>>
Type returnType = method.getGenericReturnType();
// 獲取數(shù)組[List<Chapters>]
Type[] types = returnType.getActualTypeArguments();
// 獲取List<Chapters>
Type returnType = types[0]
復(fù)制代碼

我們不能像Retrofit那樣事先給方法配置好類型,那能否通過泛型方法傳遞呢?形如:

val result = HttpUtils.get<List<Chapters>>(url, param, header)
復(fù)制代碼

如果使用Java,答案是不行。然而Kotlin可以,這基于Kotlin提供的兩個特性:

1,內(nèi)聯(lián)函數(shù)

內(nèi)聯(lián)函數(shù)會將被調(diào)用的函數(shù)體直接替換到函數(shù)調(diào)用的地方。

2,泛型reified關(guān)鍵字

reified關(guān)鍵字標記的泛型會被實化,一般配合內(nèi)聯(lián)函數(shù)使用。

首先了解一下reified關(guān)鍵字。

1,了解reified關(guān)鍵字

在Java中使用泛型的時候,無法通過泛型來得到Class,一般我們會將Class通過參數(shù)傳過去,和方案一同樣的問題。

比如在啟動一個activity時,可以給Activity添加擴展函數(shù):

fun <T : Activity> Activity.startActivity(clazz: Class<T>) {
    startActivity(Intent(this, clazz))
}
復(fù)制代碼

調(diào)用:

startActivity(Main2Activity::class.java)
復(fù)制代碼

kotlin提供的一個關(guān)鍵字reified(Reification 實化),它標記泛型使之成為實例化類型參數(shù),使抽象的東西更加具體或真實。配合inline使用可以直接獲取泛型的Class.

修改擴展函數(shù):

inline fun <reified T : Activity> Activity.startActivity() {
    startActivity(Intent(this, T::class.java))
}
復(fù)制代碼

調(diào)用:

startActivity<Main2Activity>()
復(fù)制代碼

是不是很簡(牛)單(逼),短短的一行代碼足足省了好幾個字母。

2,預(yù)研

那么如何在我們的包裝層中運用Kotlin的這一特性呢?

首先做一個簡單的測試:

// 定義
inline fun <reified T> request(url: String) {
    val clazz = T::class.java
    LogUtil.e(clazz.toString())
}

// 調(diào)用
request<List<String>>("www.baidu.com")
復(fù)制代碼

打印如下:

-->interface java.util.List

發(fā)現(xiàn)List是獲取到了,但其中的String還是被擦除了,因此對于嵌套泛型無法完整獲取其Type。What the ** ! 這該怎么辦呢?

回想一下Gson是如何解析類似List<String>這種嵌套泛型呢,在Gson的注釋中有如下代碼:

// 通過空的匿名內(nèi)部類獲取List<String>的Type
Type listType = new TypeToken<List<String>>() {}.getType();

List<String> target = new LinkedList<String>();
target.add("blah");
Gson gson = new Gson();
// 對象轉(zhuǎn)json
String json = gson.toJson(target, listType);
// json解析
List<String> target2 = gson.fromJson(json, listType);
復(fù)制代碼

它是通過空的匿名內(nèi)部類來獲取List<String>的Type,查看TypeToken的代碼可知,它和方案一采用的同種方式獲取的Type,只不過它封裝了一個類來專門處理這個問題:

class TypeToken<T>{

    final Type type;

    protected TypeToken() {
        this.type = getSuperclassTypeParameter(getClass());
    }

    static Type getSuperclassTypeParameter(Class<?> subclass) {
        Type superclass = subclass.getGenericSuperclass();
        ...
        ParameterizedType parameterized = (ParameterizedType) superclass;
        return $Gson$Types.canonicalize(parameterized.getActualTypeArguments()[0]);
    }
    ...
}
復(fù)制代碼

那我們能否用同樣的方式獲取泛型T的Type呢?試一下:

inline fun <reified T> request(url: String) {
    val type = object : TypeToken<T>() {}.type
    LogUtil.e(type.toString())
}
復(fù)制代碼

打印結(jié)果:

-->java.util.List<? extends java.lang.String>

發(fā)現(xiàn)可以獲取到其完整的Type,那就可以將其作為參數(shù)傳遞了。

3,實戰(zhàn)

// get請求
suspend inline fun <reified T> get(
    url: String,
    param: HashMap<String, Any>? = null,
    headers: HashMap<String, String>? = null
): T {
    val returnType = object : TypeToken<T>() {}.type
    return get(url, param, headers, returnType)
}

// 包裝get請求的請求參數(shù),最后通過execRequest()統(tǒng)一發(fā)起請求
suspend fun <T> get(
    url: String,
    param: HashMap<String, Any>? = null,
    headers: HashMap<String, String>? = null,
    returnType: Type
): T {
    val urlBuilder = HttpUrl.parse(url)!!.newBuilder()
    param?.let {
        it.keys.forEach { key ->
            urlBuilder.addQueryParameter(key, it[key].toString())
        }
    }
    return execRequest(
        "GET",
        urlBuilder.build(),
        headers,
        null,returnType
    )
}

// todo post,put,delete請求等

// 統(tǒng)一請求方法
suspend fun <T> execRequest(
    method: String,
    httpUrl: HttpUrl,
    headers: HashMap<String, String>? = null,
    requestBody: RequestBody?,
    returnType: Type
): T {
    val request = Request.Builder().url(httpUrl).method(method, requestBody)
    headers?.keys?.forEach {
        request.addHeader(it, headers[it])
    }
    try {
        OkHttpUtils.mClient.newCall(request).execute().use { response ->
            val body = response.body()?.string()
            val jsonObject = JSONObject(body)
            val code = jsonObject.get("errorCode")
            when (code) {
                0 -> {
                    val data = jsonObject.get("data").toString()
                    return Gson().fromJson(data, returnType)
                }
                ...
                else -> {
                    throw MyException("業(yè)務(wù)異常:?code")
                }
            }
        }
    } catch (e: Throwable) {
        throw e
    }
}
復(fù)制代碼

以上只需要對OkHttp簡單的封裝即可很方便的發(fā)起網(wǎng)絡(luò)請求:

viewModelScope.launch(Dispatchers.IO) { 
    val url = "https://wanandroid.com/wxarticle/chapters/json"
    // 請求網(wǎng)絡(luò)返回數(shù)據(jù)
    val result = HttpUtils.get<List<Chapters>>(url)
}
復(fù)制代碼

小結(jié):該方案不使用回調(diào),通過泛型方法借助Kotlin的特性實現(xiàn)返回類型Type的獲取,將解析結(jié)果直接返回。

狀態(tài)及異常統(tǒng)一處理

以下不是本文的重點,只是探討一下請求中狀態(tài)及異常如何處理,如果有不足的地方或更好的方案請不吝賜教。

由于使用Kt協(xié)程,網(wǎng)絡(luò)請求運行在協(xié)程IO中,因此使用的是OkHttp的同步請求,這就需要對網(wǎng)絡(luò)請求進行try-catch捕獲異常。

在ViewModel+LiveData的場景下,如果存在多個網(wǎng)絡(luò)請求,就會存在一個問題:需要定義多個Start/Finish以及Error等狀態(tài)的LiveData供UI層監(jiān)聽。一般這些狀態(tài)可能做的是相同的操作:Start時啟動Loading,F(xiàn)inish時關(guān)閉Loading,異常時給出異常提示。因此就需要對這些狀態(tài)進行封裝。

相關(guān)的封裝方案也有很多。這里簡單給出一個方案,僅供參考。

因為Start/Finish/Error等狀態(tài)一般是統(tǒng)一處理,那就把他們封裝到一個sealed class中。

sealed class LoadState {
    /**
     * 開始
     */
    class Start(var tip: String = "正在加載中...") : LoadState()

    /**
     * 異常
     */
    class Error(val msg: String) : LoadState()

    /**
     * 結(jié)束
     */
    object Finish : LoadState
}
復(fù)制代碼

在BaseViewModel中定義LoadState的LiveData供View層監(jiān)聽:

open class BaseViewModel() : ViewModel() {

    // 加載狀態(tài)
    val loadState = MutableLiveData<LoadState>()
    ...
}
復(fù)制代碼

UI中:

viewModel.loadState.observe(this) {
    when (it) {
        is LoadState.Start -> {
            // todo 開始加載
        }
        is LoadState.Error -> {
            // todo 加載失敗
        }
        is LoadState.Finish -> {
            // todo 加載完成
        }
    }
}
復(fù)制代碼

有了觀察者和被觀察者,那何時分發(fā)數(shù)據(jù)呢?

還是在BaseViewModel中將網(wǎng)絡(luò)請求通過高階函數(shù)的方式在協(xié)程中執(zhí)行,如下:

open class BaseViewModel() : ViewModel() {

    // 加載狀態(tài)
    val loadState = MutableLiveData<LoadState>()

    // 通過該方法發(fā)起網(wǎng)絡(luò)請求
    private fun launch(block: suspend CoroutineScope.() -> Unit) {
        viewModelScope.launch() {
            try {
                withContext(Dispatchers.IO) {
                    // 執(zhí)行網(wǎng)絡(luò)請求代碼塊
                    block.invoke(this)
                }
            } catch (e: Throwable) {
                // handle error
                val error = ExceptionUtils.parseException(e)
                loadState.value = LoadState.Error(error)
            } finally {
                loadState.value = LoadState.Finish
            }
        }
    }
}
復(fù)制代碼

在ViewModel中:

val chapters = MutableLiveData<List<Chapters>>()

fun request(){
    launch {
        val url = "https://wanandroid.com/wxarticle/chapters/json"
        // 請求網(wǎng)絡(luò)返回數(shù)據(jù)
        val result = HttpUtils.get<List<Chapters>>(url)
        chapters.postValue(result)
    }
}
復(fù)制代碼

這么一來在ViewModel中只需要通過launch函數(shù)發(fā)起網(wǎng)絡(luò)請求就可以讓請求在IO線程中執(zhí)行,并且自動分發(fā)Start/Finish/Error等狀態(tài)。

在UI中只需要監(jiān)聽LoadState以及網(wǎng)絡(luò)請求返回結(jié)果的LiveData即可。

至此在Kotlin+ViewModel+LiveData環(huán)境下簡單的網(wǎng)絡(luò)請求封裝已經(jīng)完成,但還存在一些問題:

問題1:

有些請求是在后臺靜默執(zhí)行,不需要處理開始結(jié)束異常的狀態(tài)。

此時可以從BaseViewModel中解決,同launch函數(shù)一樣,添加launchSlient函數(shù),其中控制是否分發(fā)LoadState狀態(tài)。

問題2:

同時發(fā)起多個請求,期間都需要顯示Loading,但是某一個先完成了,就回調(diào)了LoadState.Finish,導致其他請求還在進行中但Loading已經(jīng)關(guān)閉了。

可以在BaseViewModel中通過原子操作AtomicInteger,記錄當前請求中的數(shù)量,可以在其數(shù)量為0時回調(diào)LoadState.Finish。

問題3:

對于一些業(yè)務(wù)異??赡苄枰厥馓幚?,不能在統(tǒng)一的方式中處理。

此時需要在包裝層處理,可將統(tǒng)一異常處理作為兜底策略,對于特殊的業(yè)務(wù)異常,捕獲后不向外拋出,通過高階函數(shù)方式處理:

launch {
    val url = "https://wanandroid.com/wxarticle/chapters/json"
    // 請求網(wǎng)絡(luò)返回數(shù)據(jù)
    val result = HttpUtils.get<List<Chapters>>(url){ error->
        // todo 處理異常
    }
}
復(fù)制代碼

總結(jié)

本文重點探討了在kotlin+協(xié)程+viewmodel+livedata環(huán)境下通過對OkHttp的包裝讓網(wǎng)絡(luò)請求更加簡單的方案。

當然這并不是說可以完全拋棄Retrofit,Retrofit是一個大而全的網(wǎng)絡(luò)請求封裝,能夠滿足各種需求。

文中只用了幾十行代碼實現(xiàn)了get請求,詳細代碼及其他封裝可參考本人的開源項目風云天氣github.com/wdsqjq/Feng…

以上方案適用于簡單的網(wǎng)絡(luò)請求場景,對于特殊的需求還需要自行擴展

B站視頻教程:【Android開發(fā)教程】Android開發(fā)——網(wǎng)絡(luò)訪問框架源碼解析合集

?著作權(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ù)。

相關(guān)閱讀更多精彩內(nèi)容

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