Kotlin 協(xié)程+Retrofit 最優(yōu)雅的網(wǎng)絡請求使用

Kotlin 協(xié)程+Retrofit 最優(yōu)雅的網(wǎng)絡請求使用

1.簡介

Retrofit對協(xié)程的支持非常的簡陋。在kotlin中使用不符合kotlin的優(yōu)雅

interface TestServer {
    @GET("banner/json")
    suspend fun banner(): ApiResponse<List<Banner>>
}

//實現(xiàn)并行捕獲異常的網(wǎng)絡請求
 fun oldBanner(){
        viewModelScope.launch {
            //傳統(tǒng)模式使用retrofit需要try catch

            val bannerAsync1 = async {
                var result : ApiResponse<List<Banner>>? = null
                kotlin.runCatching {
                   service.banner()
                }.onFailure {
                    Log.e("banner",it.toString())
                }.onSuccess {
                    result = it 
                }
                result
            }

            val bannerAsync2 = async {
                var result : ApiResponse<List<Banner>>? = null
                kotlin.runCatching {
                    service.banner()
                }.onFailure {
                    Log.e("banner",it.toString())
                }.onSuccess {
                    result = it
                }
                result
            }

            bannerAsync1.await()
            bannerAsync2.await()
        }
    }

一層嵌套一層键俱,屬實無法忍受峡迷。kotlin應該一行代碼解決問題敞咧,才符合kotlin的優(yōu)雅

使用本框架后

interface TestServer {
    @GET("banner/json")
    suspend fun awaitBanner(): Await<List<Banner>>
}

   //實現(xiàn)并行捕獲異常的網(wǎng)絡請求
fun parallel(){
     viewModelScope.launch {
     val awaitBanner1 = service.awaitBanner().tryAsync(this)
     val awaitBanner2 = service.awaitBanner().tryAsync(this)

      //兩個接口一起調(diào)用
      awaitBanner1.await()
      awaitBanner2.await()
   }
}

2.源碼地址

GitHub

3.查看Retrofit源碼

先看Retrofit create方法

public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    return (T)
        Proxy.newProxyInstance(
            service.getClassLoader(),
            new Class<?>[] {service},
            new InvocationHandler() {
              private final Platform platform = Platform.get();
              private final Object[] emptyArgs = new Object[0];

              @Override
              public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                  throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args);
                }
                args = args != null ? args : emptyArgs;
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);//具體調(diào)用
              }
            });
  }

loadServiceMethod(method).invoke(args)進入這個方法看具體調(diào)用

我們查看suspenForResponse中的adapt

@Override
    protected Object adapt(Call<ResponseT> call, Object[] args) {
      call = callAdapter.adapt(call);//如果用戶不設置callAdapterFactory就使用DefaultCallAdapterFactory

      //noinspection unchecked Checked by reflection inside RequestFactory.
      Continuation<Response<ResponseT>> continuation =
          (Continuation<Response<ResponseT>>) args[args.length - 1];

      // See SuspendForBody for explanation about this try/catch.
      try {
        return KotlinExtensions.awaitResponse(call, continuation);
      } catch (Exception e) {
        return KotlinExtensions.suspendAndThrow(e, continuation);
      }
    }
  }

后面直接交給協(xié)程去調(diào)用call。具體的okhttp調(diào)用在DefaultCallAdapterFactory障般「苛或者用戶自定義的callAdapterFactory中

因此我們這邊可以自定義CallAdapterFactory在調(diào)用后不進行網(wǎng)絡請求的訪問棘幸,在用戶調(diào)用具體方法時候再進行網(wǎng)絡請求訪問。

4.自定義CallAdapterFactory

Retrofit在調(diào)用后直接進行了網(wǎng)絡請求倦零,因此很不好操作够话。我們把網(wǎng)絡請求的控制權放在我們手里蓝翰,就能隨意操作。

class ApiResultCallAdapterFactory : CallAdapter.Factory() {
    override fun get(returnType: Type, annotations: Array<Annotation>, retrofit: Retrofit): CallAdapter<*, *>? {
        //檢查returnType是否是Call<T>類型的
        if (getRawType(returnType) != Call::class.java) return null
        check(returnType is ParameterizedType) { "$returnType must be parameterized. Raw types are not supported" }
        //取出Call<T> 里的T女嘲,檢查是否是Await<T>
        val apiResultType = getParameterUpperBound(0, returnType)
        // 如果不是 Await 則不由本 CallAdapter.Factory 處理 兼容正常模式
        if (getRawType(apiResultType) != Await::class.java) return null
        check(apiResultType is ParameterizedType) { "$apiResultType must be parameterized. Raw types are not supported" }

        //取出Await<T>中的T 也就是API返回數(shù)據(jù)對應的數(shù)據(jù)類型
//        val dataType = getParameterUpperBound(0, apiResultType)

        return ApiResultCallAdapter<Any>(apiResultType)
    }

}

class ApiResultCallAdapter<T>(private val type: Type) : CallAdapter<T, Call<Await<T>>> {
    override fun responseType(): Type = type

    override fun adapt(call: Call<T>): Call<Await<T>> {
        return ApiResultCall(call)
    }
}

class ApiResultCall<T>(private val delegate: Call<T>) : Call<Await<T>> {
    /**
     * 該方法會被Retrofit處理suspend方法的代碼調(diào)用畜份,并傳進來一個callback,如果你回調(diào)了callback.onResponse,那么suspend方法就會成功返回
     * 如果你回調(diào)了callback.onFailure那么suspend方法就會拋異常
     *
     * 所以我們這里的實現(xiàn)是回調(diào)callback.onResponse,將okhttp的call delegate
     */
    override fun enqueue(callback: Callback<Await<T>>) {
        //將okhttp call放入AwaitImpl直接返回欣尼,不做網(wǎng)絡請求爆雹。在調(diào)用AwaitImpl的await時才真正開始網(wǎng)絡請求
        callback.onResponse(this@ApiResultCall, Response.success(delegate.toResponse()))
    }
}

internal class AwaitImpl<T>(
    private val call : Call<T>,
) : Await<T> {

    override suspend fun await(): T {

        return try {
            call.await()
        } catch (t: Throwable) {
            throw t
        }
    }
}

通過上面自定義callAdapter后,我們延遲了網(wǎng)絡請求愕鼓,在調(diào)用Retrofit后并不會請求網(wǎng)絡钙态,只會將網(wǎng)絡請求所需要的call的放入await中。

   @GET("banner/json")
    suspend fun awaitBanner(): Await<List<Banner>>

我們拿到的Await<List>并沒有做網(wǎng)絡請求菇晃。在這個實體類中包含了okHttp的call册倒。

這時候我們可以定義如下方法就能捕獲異常

suspend fun <T> Await<T>.tryAsync(
    scope: CoroutineScope,
    onCatch: ((Throwable) -> Unit)? = null,
    context: CoroutineContext = SupervisorJob(scope.coroutineContext[Job]),
    start: CoroutineStart = CoroutineStart.DEFAULT
): Deferred<T?> = scope.async(context, start) {
    try {
        await()
    } catch (e: Throwable) {
        onCatch?.invoke(e)
        null
    }
}

同樣并行捕獲異常的請求,就可以通過如下方式調(diào)用磺送,優(yōu)雅簡潔了很多

   /**
     * 并行 async
     */
    fun parallel(){
        viewModelScope.launch {
            val awaitBanner1 = service.awaitBanner().tryAsync(this)
            val awaitBanner2 = service.awaitBanner().tryAsync(this)

            //兩個接口一起調(diào)用
            awaitBanner1.await()
            awaitBanner2.await()
        }
    }

這時候我們發(fā)現(xiàn)網(wǎng)絡請求成功了驻子,解析數(shù)據(jù)失敗。因為我們在數(shù)據(jù)外面套了一層await估灿〕绾牵肯定無法解析成功。

本著哪里錯誤解決哪里的思路馅袁,我們自定義Gson解析

5.自定義Gson解析

class GsonConverterFactory private constructor(private var responseCz : Class<*>,var responseConverter : GsonResponseBodyConverter, private val gson: Gson) : Converter.Factory() {

    override fun responseBodyConverter(
        type: Type, annotations: Array<Annotation>,
        retrofit: Retrofit
    ): Converter<ResponseBody, *> {
        var adapter : TypeAdapter<*>? = null
        //檢查是否是Await<T>
        if (Utils.getRawType(type) == Await::class.java && type is ParameterizedType){
            //取出Await<T>中的T
            val awaitType =  Utils.getParameterUpperBound(0, type)
            if(awaitType != null){
                adapter = gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,awaitType]))
            }
        }
        //不是awiat正常解析域慷,兼容正常模式
        if(adapter == null){
            adapter= gson.getAdapter(TypeToken.get(ParameterizedTypeImpl[responseCz,type]))
        }
        return responseConverter.init(gson, adapter!!)
    }
}

class MyGsonResponseBodyConverter : GsonResponseBodyConverter() {

    override fun convert(value: ResponseBody): Any {
        val jsonReader = gson.newJsonReader(value.charStream())
        val data = adapter.read(jsonReader) as ApiResponse<*>
        val t = data.data

        val listData = t as? ApiPagerResponse<*>
        if (listData != null) {
            //如果返回值值列表封裝類,且是第一頁并且空數(shù)據(jù) 那么給空異常 讓界面顯示空
            if (listData.isRefresh() && listData.isEmpty()) {
                throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
            }
        }

        // errCode 不等于 SUCCESS_CODE汗销,拋出異常
        if (data.errorCode != NetConstant.SUCCESS_CODE) {
            throw ParseException(data.errorCode, data.errorMsg)
        }

        return t!!
    }

}

6.本框架使用

添加依賴


maven central 1.1.0

implementation "io.github.cnoke.ktnet:api:?"

寫一個網(wǎng)絡請求數(shù)據(jù)基類

open class ApiResponse<T>(
    var data: T? = null,
    var errorCode: String = "",
    var errorMsg: String = ""
)

實現(xiàn)com.cnoke.net.factory.GsonResponseBodyConverter

class MyGsonResponseBodyConverter : GsonResponseBodyConverter() {

    override fun convert(value: ResponseBody): Any {
        val jsonReader = gson.newJsonReader(value.charStream())
        val data = adapter.read(jsonReader) as ApiResponse<*>
        val t = data.data

        val listData = t as? ApiPagerResponse<*>
        if (listData != null) {
            //如果返回值值列表封裝類犹褒,且是第一頁并且空數(shù)據(jù) 那么給空異常 讓界面顯示空
            if (listData.isRefresh() && listData.isEmpty()) {
                throw ParseException(NetConstant.EMPTY_CODE, data.errorMsg)
            }
        }

        // errCode 不等于 SUCCESS_CODE,拋出異常
        if (data.errorCode != NetConstant.SUCCESS_CODE) {
            throw ParseException(data.errorCode, data.errorMsg)
        }

        return t!!
    }

}

進行網(wǎng)絡請求

interface TestServer {
    @GET("banner/json")
    suspend fun awaitBanner(): Await<List<Banner>>
}

val okHttpClient = OkHttpClient.Builder()
            .addInterceptor(HeadInterceptor())
            .addInterceptor(LogInterceptor())
            .build()

val retrofit = Retrofit.Builder()
            .client(okHttpClient)
            .baseUrl("https://www.wanandroid.com/")
            .addCallAdapterFactory(ApiResultCallAdapterFactory())
            .addConverterFactory(GsonConverterFactory.create(ApiResponse::class.java,MyGsonResponseBodyConverter()))
            .build()
val service: TestServer = retrofit.create(TestServer::class.java)
lifecycleScope.launch {
       val banner = service.awaitBanner().await()
}

異步請求同步請求弛针,異常捕獲參考如下try開頭的會捕獲異常化漆,非try開頭不會捕獲。

fun banner(){
    lifecycleScope.launch {
        //單獨處理異常 tryAwait會處理異常钦奋,如果異常返回空
        val awaitBanner = service.awaitBanner().tryAwait()
        awaitBanner?.let {
            for(banner in it){
                Log.e("awaitBanner",banner.title)
            }
        }

        /**
         * 不處理異常 異常會直接拋出,統(tǒng)一處理
         */
        val awaitBannerError = service.awaitBanner().await()
    }
}

/**
 * 串行 await
 */
fun serial(){
    lifecycleScope.launch {
        //先調(diào)用第一個接口await
        val awaitBanner1 = service.awaitBanner().await()
        //第一個接口完成后調(diào)用第二個接口
        val awaitBanner2 = service.awaitBanner().await()
    }
}

/**
 * 并行 async
 */
fun parallel(){
    lifecycleScope.launch {
        val awaitBanner1 = service.awaitBanner().async(this)
        val awaitBanner2 = service.awaitBanner().async(this)

        //兩個接口一起調(diào)用
        awaitBanner1.await()
        awaitBanner2.await()
    }
}

?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末疙赠,一起剝皮案震驚了整個濱河市付材,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌圃阳,老刑警劉巖厌衔,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異捍岳,居然都是意外死亡富寿,警方通過查閱死者的電腦和手機睬隶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來页徐,“玉大人苏潜,你說我怎么就攤上這事”溆拢” “怎么了恤左?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵,是天一觀的道長搀绣。 經(jīng)常有香客問我飞袋,道長,這世上最難降的妖魔是什么链患? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任巧鸭,我火速辦了婚禮,結果婚禮上麻捻,老公的妹妹穿的比我還像新娘纲仍。我一直安慰自己,他們只是感情好芯肤,可當我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布巷折。 她就那樣靜靜地躺著,像睡著了一般崖咨。 火紅的嫁衣襯著肌膚如雪锻拘。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天击蹲,我揣著相機與錄音署拟,去河邊找鬼。 笑死歌豺,一個胖子當著我的面吹牛推穷,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播类咧,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼馒铃,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了痕惋?” 一聲冷哼從身側響起区宇,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎值戳,沒想到半個月后议谷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡堕虹,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年卧晓,在試婚紗的時候發(fā)現(xiàn)自己被綠了芬首。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡逼裆,死狀恐怖郁稍,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情波附,我是刑警寧澤艺晴,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布,位于F島的核電站掸屡,受9級特大地震影響封寞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜仅财,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一狈究、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧盏求,春花似錦抖锥、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至荆烈,卻和暖如春拯勉,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背憔购。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工宫峦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人玫鸟。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓导绷,卻偏偏與公主長得像,于是被迫代替她去往敵國和親屎飘。 傳聞我的和親對象是個殘疾皇子妥曲,可洞房花燭夜當晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容