版權(quán)聲明:本文為博主原創(chuàng)文章掌逛,遵循 CC 4.0 BY-SA 版權(quán)協(xié)議,轉(zhuǎn)載請(qǐng)附上原文出處鏈接和本聲明壁袄。
本文鏈接:[http://www.reibang.com/p/dc9f28f63666)
前言
Retrofit是當(dāng)前業(yè)內(nèi)非常流行的網(wǎng)絡(luò)請(qǐng)求框架类早,它簡(jiǎn)單易用,幾乎是Android開(kāi)發(fā)必備嗜逻。同時(shí)它使用到了大量的設(shè)計(jì)模式涩僻,其代碼值得我們仔細(xì)研讀,其設(shè)計(jì)思想值得我們深入思考栈顷。逆日。。
為了防止大佬們噴我萄凤,先吹一波Retrofit室抽,接下來(lái)探討一下在Kotlin+協(xié)程環(huán)境下比Retrofit更簡(jiǎn)單易用的封裝方式。
首先提出兩個(gè)問(wèn)題:
-
有必要對(duì)網(wǎng)絡(luò)請(qǐng)求做一層包裝嗎靡努?
個(gè)人感覺(jué)很有必要坪圾,就拿Retrofit來(lái)說(shuō)對(duì)業(yè)務(wù)代碼的侵入是比較大的,從長(zhǎng)遠(yuǎn)的角度來(lái)考慮惑朦,沒(méi)有任何框架敢說(shuō)自己是yyds神年,一旦有一天出了新技術(shù)想要換框架,面對(duì)那么多的業(yè)務(wù)代碼不由得腦海中就會(huì)有一萬(wàn)頭羊駝奔騰而過(guò)行嗤。
-
如果做包裝層已日,那Retrofit還有使用的必要嗎?
首先為什么要使用Retrofit栅屏?它相較于OkHttp主要做了三件事情:1飘千,線程切換堂鲜。2,數(shù)據(jù)解析护奈。3缔莲,請(qǐng)求參數(shù)可配置化。
對(duì)于線程切換我們用Kt協(xié)程可以很容易實(shí)現(xiàn)霉旗。對(duì)于數(shù)據(jù)解析如果能夠拿到返回結(jié)果的Type,也就是一行代碼的事情痴奏。對(duì)于請(qǐng)求參數(shù),通過(guò)簡(jiǎn)單的封裝也可以很方便設(shè)置厌秒。
因此本文的目的就是扔掉Retrofit读拆,借助Kt協(xié)程對(duì)OkHttp做一層包裝。
先看一下最終使用效果:
viewModelScope.launch(Dispatchers.IO) {
// 感謝玩安卓提供的api
val url = "https://wanandroid.com/wxarticle/chapters/json"
// 請(qǐng)求網(wǎng)絡(luò)返回?cái)?shù)據(jù)
val result = HttpUtils.get<List<Chapters>>(url)
}
明確目的
對(duì)于一個(gè)網(wǎng)絡(luò)請(qǐng)求工具我們想要的無(wú)非就是我們把參數(shù)傳進(jìn)去鸵闪,然后把期望的結(jié)果返回來(lái)檐晕。
此時(shí)心中應(yīng)該大概有了想要的效果:
HttpUtils.get(url, param, header, object: Callback<T>{
onSuccess{}
onError{}
})
HttpUtils.post(url, body, header, object: Callback<T>{
onSuccess{}
onError{}
})
要傳的參數(shù)包括url,Query參數(shù)蚌讼,Body辟灰,Header等,另外為了使其返回?cái)?shù)據(jù)解析后的結(jié)果篡石,還需要傳結(jié)果類型的Type芥喇。其他參數(shù)都好說(shuō),唯獨(dú)返回結(jié)果的Type該怎么傳遞呢凰萨?接下來(lái)的兩個(gè)方案都是圍繞這個(gè)問(wèn)題展開(kāi)的乃坤。
方案一
首先定義一下返回結(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
)
前面已經(jīng)提到,重點(diǎn)在于如何傳遞期望返回類型的Type沟蔑。
如果是對(duì)象如ChaptersResp
湿诊,我們可以通過(guò)ChaptersResp.class
作為參數(shù)傳遞,但是項(xiàng)目中一般都會(huì)有一個(gè)BaseResp
瘦材,因此只需要定義一個(gè)Chapters
就可以了厅须,那問(wèn)題來(lái)了,如何傳遞List<Chapters>
呢食棕?總不能傳個(gè)(List<Chapters>).class
吧朗和。
1,Object.class方式
如果非要通過(guò)這種Object.class
方式傳遞簿晓,有兩種方式:1眶拉,傳遞完整對(duì)象的class,如:ChaptersResp.class
憔儿。2忆植,從方法上解決,將單體對(duì)象和集合對(duì)象分開(kāi),如返回結(jié)果是單體對(duì)象就用HttpUtils.get()朝刊,集合對(duì)象就用HttpUtils.getList()耀里,然后在方法內(nèi)部進(jìn)行區(qū)分。如下:
// 1拾氓,傳參上解決:傳整體對(duì)象
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{}
})
這應(yīng)該是最low的方式了咙鞍,只有新手才會(huì)這么搞吧房官,如果有更好的方案,這種當(dāng)然不可取续滋。
從上面的方法可以看出在回調(diào)中通過(guò)泛型已經(jīng)添加了期望返回的類型翰守。那能否從其中獲取呢?
2吃粒,泛型中獲取
在泛型類中可以通過(guò)getGenericSuperclass()
獲取當(dāng)前類表示的實(shí)體(類潦俺,接口拒课,基本類型或void)的直接父類的Type徐勃,通過(guò)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();
// 由于這里是有一個(gè)泛型T,因此取第一個(gè)就是傳進(jìn)來(lái)的泛型的Type
T result = new Gson().fromJson(data, types[0]);
onSuccess(result, "" + data.getCode(), data.getMsg());
} else {
throw new ClassCastException();
}
}
}
在拿到請(qǐng)求結(jié)果以后通過(guò)回調(diào)的onNext()
將返回結(jié)果交給回調(diào)來(lái)處理早像。在onNext()
中解析完數(shù)據(jù)再調(diào)用onSuccess()
等其他回調(diào)僻肖。
這么一來(lái)請(qǐng)求就可以這么寫:
HttpUtils.get(url, param, object: HttpCallback<List<Chapters>> {
onSuccess{}
onError{}
})
小結(jié):該方案使用回調(diào)的方式,通過(guò)回調(diào)這個(gè)匿名內(nèi)部獲取返回類型的Type卢鹦,最終把解析后的結(jié)果回調(diào)出來(lái)臀脏。
方案二
由于這里使用到了Kt協(xié)程,Kt協(xié)程允許我們以同步的方式實(shí)現(xiàn)異步的效果冀自,這似乎跟回調(diào)有點(diǎn)不搭揉稚,那如何像Retrofit一樣直接返回結(jié)果呢?
由于沒(méi)有了回調(diào)對(duì)象熬粗,此時(shí)面臨的問(wèn)題依然是返回結(jié)果的類型的傳遞搀玖。
Retrofit是在接口方法上配置的返回類型,在動(dòng)態(tài)代理中通過(guò)調(diào)用method的getGenericReturnType()
可以獲取到方法返回類型的Type驻呐,進(jìn)而可以獲取到期望返回類型的Type灌诅。
// 獲取到Call<List<Chapters>>
Type returnType = method.getGenericReturnType();
// 獲取數(shù)組[List<Chapters>]
Type[] types = returnType.getActualTypeArguments();
// 獲取List<Chapters>
Type returnType = types[0]
我們不能像Retrofit那樣事先給方法配置好類型,那能否通過(guò)泛型方法傳遞呢含末?形如:
val result = HttpUtils.get<List<Chapters>>(url, param, header)
如果使用Java猜拾,答案是不行。然而Kotlin可以佣盒,這基于Kotlin提供的兩個(gè)特性:
1璃弄,內(nèi)聯(lián)函數(shù)
內(nèi)聯(lián)函數(shù)會(huì)將被調(diào)用的函數(shù)體直接替換到函數(shù)調(diào)用的地方眯娱。
2蹋绽,泛型reified
關(guān)鍵字
被reified
關(guān)鍵字標(biāo)記的泛型會(huì)被實(shí)化溜族,一般配合內(nèi)聯(lián)函數(shù)使用。
首先了解一下reified
關(guān)鍵字蜀变。
1,了解reified
關(guān)鍵字
在Java中使用泛型的時(shí)候,無(wú)法通過(guò)泛型來(lái)得到Class嗦明,一般我們會(huì)將Class通過(guò)參數(shù)傳過(guò)去,和方案一同樣的問(wèn)題蚪燕。
比如在啟動(dòng)一個(gè)activity時(shí)娶牌,可以給Activity添加擴(kuò)展函數(shù):
fun <T : Activity> Activity.startActivity(clazz: Class<T>) {
startActivity(Intent(this, clazz))
}
調(diào)用:
startActivity(Main2Activity::class.java)
kotlin提供的一個(gè)關(guān)鍵字reified
(Reification 實(shí)化),它標(biāo)記泛型使之成為實(shí)例化類型參數(shù)馆纳,使抽象的東西更加具體或真實(shí)诗良。配合inline
使用可以直接獲取泛型的Class.
修改擴(kuò)展函數(shù):
inline fun <reified T : Activity> Activity.startActivity() {
startActivity(Intent(this, T::class.java))
}
調(diào)用:
startActivity<Main2Activity>()
是不是很簡(jiǎn)(牛)單(逼),短短的一行代碼足足省了好幾個(gè)字母鲁驶。
2鉴裹,預(yù)研
那么如何在我們的包裝層中運(yùn)用Kotlin的這一特性呢?
首先做一個(gè)簡(jiǎn)單的測(cè)試:
// 定義
inline fun <reified T> request(url: String) {
val clazz = T::class.java
LogUtil.e(clazz.toString())
}
// 調(diào)用
request<List<String>>("www.baidu.com")
打印如下:
-->interface java.util.List
發(fā)現(xiàn)List
是獲取到了钥弯,但其中的String
還是被擦除了径荔,因此對(duì)于嵌套泛型無(wú)法完整獲取其Type。What the ** ! 這該怎么辦呢脆霎?
回想一下Gson是如何解析類似List<String>
這種嵌套泛型呢总处,在Gson的注釋中有如下代碼:
// 通過(guò)空的匿名內(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();
// 對(duì)象轉(zhuǎn)json
String json = gson.toJson(target, listType);
// json解析
List<String> target2 = gson.fromJson(json, listType);
它是通過(guò)空的匿名內(nèi)部類來(lái)獲取List<String>
的Type,查看TypeToken的代碼可知睛蛛,它和方案一采用的同種方式獲取的Type鹦马,只不過(guò)它封裝了一個(gè)類來(lái)專門處理這個(gè)問(wèn)題:
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]);
}
...
}
那我們能否用同樣的方式獲取泛型T的Type呢?試一下:
inline fun <reified T> request(url: String) {
val type = object : TypeToken<T>() {}.type
LogUtil.e(type.toString())
}
打印結(jié)果:
-->java.util.List<? extends java.lang.String>
發(fā)現(xiàn)可以獲取到其完整的Type忆肾,那就可以將其作為參數(shù)傳遞了荸频。
3,實(shí)戰(zhàn)
// get請(qǐng)求
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請(qǐng)求的請(qǐng)求參數(shù)客冈,最后通過(guò)execRequest()統(tǒng)一發(fā)起請(qǐng)求
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請(qǐng)求等
// 統(tǒng)一請(qǐ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
}
}
以上只需要對(duì)OkHttp簡(jiǎn)單的封裝即可很方便的發(fā)起網(wǎng)絡(luò)請(qǐng)求:
viewModelScope.launch(Dispatchers.IO) {
val url = "https://wanandroid.com/wxarticle/chapters/json"
// 請(qǐng)求網(wǎng)絡(luò)返回?cái)?shù)據(jù)
val result = HttpUtils.get<List<Chapters>>(url)
}
其他的請(qǐng)求方式如post,put郊酒,delete等只需要根據(jù)其請(qǐng)求特點(diǎn)稍加處理最后統(tǒng)一調(diào)用的execRequest()
方法即可遇绞。
小結(jié):該方案不使用回調(diào),通過(guò)泛型方法借助Kotlin的特性實(shí)現(xiàn)返回類型Type的獲取燎窘,將解析結(jié)果直接返回摹闽。
狀態(tài)及異常統(tǒng)一處理
以下不是本文的重點(diǎn),只是探討一下請(qǐng)求中狀態(tài)及異常如何處理褐健,如果有不足的地方或更好的方案請(qǐng)不吝賜教付鹿。
由于使用Kt協(xié)程澜汤,網(wǎng)絡(luò)請(qǐng)求運(yùn)行在協(xié)程IO中,因此使用的是OkHttp的同步請(qǐng)求舵匾,這就需要對(duì)網(wǎng)絡(luò)請(qǐng)求進(jìn)行try-catch捕獲異常俊抵。
在ViewModel+LiveData的場(chǎng)景下,如果存在多個(gè)網(wǎng)絡(luò)請(qǐng)求坐梯,就會(huì)存在一個(gè)問(wèn)題:需要定義多個(gè)Start/Finish以及Error等狀態(tài)的LiveData供UI層監(jiān)聽(tīng)徽诲。一般這些狀態(tài)可能做的是相同的操作:Start時(shí)啟動(dòng)Loading,F(xiàn)inish時(shí)關(guān)閉Loading吵血,異常時(shí)給出異常提示谎替。因此就需要對(duì)這些狀態(tài)進(jìn)行封裝。
相關(guān)的封裝方案也有很多蹋辅。這里簡(jiǎn)單給出一個(gè)方案钱贯,僅供參考。
因?yàn)镾tart/Finish/Error等狀態(tài)一般是統(tǒng)一處理侦另,那就把他們封裝到一個(gè)sealed class中秩命。
sealed class LoadState {
/**
* 開(kāi)始
*/
class Start(var tip: String = "正在加載中...") : LoadState()
/**
* 異常
*/
class Error(val msg: String) : LoadState()
/**
* 結(jié)束
*/
object Finish : LoadState
}
在BaseViewModel中定義LoadState的LiveData供View層監(jiān)聽(tīng):
open class BaseViewModel() : ViewModel() {
// 加載狀態(tài)
val loadState = MutableLiveData<LoadState>()
...
}
UI中:
viewModel.loadState.observe(this) {
when (it) {
is LoadState.Start -> {
// todo 開(kāi)始加載
}
is LoadState.Error -> {
// todo 加載失敗
}
is LoadState.Finish -> {
// todo 加載完成
}
}
}
有了觀察者和被觀察者,那何時(shí)分發(fā)數(shù)據(jù)呢褒傅?
還是在BaseViewModel中將網(wǎng)絡(luò)請(qǐng)求通過(guò)高階函數(shù)的方式在協(xié)程中執(zhí)行弃锐,如下:
open class BaseViewModel() : ViewModel() {
// 加載狀態(tài)
val loadState = MutableLiveData<LoadState>()
// 通過(guò)該方法發(fā)起網(wǎng)絡(luò)請(qǐng)求
private fun launch(block: suspend CoroutineScope.() -> Unit) {
viewModelScope.launch() {
try {
loadState.value = LoadState.Start()
withContext(Dispatchers.IO) {
// 執(zhí)行網(wǎng)絡(luò)請(qǐng)求代碼塊
block.invoke(this)
}
} catch (e: Throwable) {
// handle error
val error = ExceptionUtils.parseException(e)
loadState.value = LoadState.Error(error)
} finally {
loadState.value = LoadState.Finish
}
}
}
}
在ViewModel中:
val chapters = MutableLiveData<List<Chapters>>()
fun request(){
launch {
val url = "https://wanandroid.com/wxarticle/chapters/json"
// 請(qǐng)求網(wǎng)絡(luò)返回?cái)?shù)據(jù)
val result = HttpUtils.get<List<Chapters>>(url)
chapters.postValue(result)
}
}
這么一來(lái)在ViewModel中只需要通過(guò)launch
函數(shù)發(fā)起網(wǎng)絡(luò)請(qǐng)求就可以讓請(qǐng)求在IO線程中執(zhí)行,并且自動(dòng)分發(fā)Start/Finish/Error等狀態(tài)樊卓。
在UI中只需要監(jiān)聽(tīng)LoadState以及網(wǎng)絡(luò)請(qǐng)求返回結(jié)果的LiveData即可拿愧。
至此在Kotlin+ViewModel+LiveData環(huán)境下簡(jiǎn)單的網(wǎng)絡(luò)請(qǐng)求封裝已經(jīng)完成杠河,但還存在一些問(wèn)題:
問(wèn)題1:
有些請(qǐng)求是在后臺(tái)靜默執(zhí)行碌尔,不需要處理開(kāi)始結(jié)束異常的狀態(tài)。
此時(shí)可以從BaseViewModel中解決券敌,同launch
函數(shù)一樣唾戚,添加launchSlient
函數(shù),其中控制是否分發(fā)LoadState
狀態(tài)待诅。
問(wèn)題2:
同時(shí)發(fā)起多個(gè)請(qǐng)求叹坦,期間都需要顯示Loading,但是某一個(gè)先完成了卑雁,就回調(diào)了LoadState.Finish
募书,導(dǎo)致其他請(qǐng)求還在進(jìn)行中但Loading已經(jīng)關(guān)閉了。
可以在BaseViewModel中通過(guò)原子操作AtomicInteger
测蹲,記錄當(dāng)前請(qǐng)求中的數(shù)量莹捡,可以在其數(shù)量為0時(shí)分發(fā)LoadState.Finish
。
問(wèn)題3:
對(duì)于一些業(yè)務(wù)異晨奂祝可能需要特殊處理篮赢,不能在統(tǒng)一的方式中處理。
此時(shí)需要在包裝層處理,可將統(tǒng)一異常處理作為兜底策略启泣,對(duì)于特殊的業(yè)務(wù)異常涣脚,捕獲后不向外拋出,通過(guò)高階函數(shù)方式處理:
launch {
val url = "https://wanandroid.com/wxarticle/chapters/json"
// 請(qǐng)求網(wǎng)絡(luò)返回?cái)?shù)據(jù)
val result = HttpUtils.get<List<Chapters>>(url){ error ->
// todo 處理異常
}
}
總結(jié)
本文重點(diǎn)探討了在kotlin+協(xié)程+viewmodel+livedata環(huán)境下通過(guò)對(duì)OkHttp的包裝讓網(wǎng)絡(luò)請(qǐng)求更加簡(jiǎn)單的方案寥茫。
當(dāng)然這并不是說(shuō)可以完全拋棄Retrofit遣蚀,Retrofit是一個(gè)大而全的網(wǎng)絡(luò)請(qǐng)求封裝,能夠滿足各種需求纱耻。
文中只用了幾十行代碼實(shí)現(xiàn)了get請(qǐng)求妙同,詳細(xì)代碼及其他封裝可參考本人的開(kāi)源項(xiàng)目風(fēng)云天氣(https://github.com/wdsqjq/FengYunWeather )
以上方案適用于簡(jiǎn)單的網(wǎng)絡(luò)請(qǐng)求場(chǎng)景,對(duì)于特殊的需求還需要自行擴(kuò)展膝迎。