在kotlin協(xié)程中使用自定義CallAdapter處理錯(cuò)誤

添加依賴

implementation 'com.squareup.retrofit2:retrofit:2.11.0'
implementation 'com.squareup.retrofit2:converter-gson:2.11.0'
implementation 'com.squareup.okhttp3:okhttp:4.12.0'

由于需要對(duì) OKHttpClient 做一些操作和定制,這里添加了 okhttp 的依賴。實(shí)體類的轉(zhuǎn)換使用了 gson,為啥用 gson,問就是項(xiàng)目里面就是用的 gson,后面再介紹一下其他的converter。

  • Gson: com.squareup.retrofit2:converter-gson
  • Jackson: com.squareup.retrofit2:converter-jackson
  • Moshi: com.squareup.retrofit2:converter-moshi
  • Protobuf: com.squareup.retrofit2:converter-protobuf
  • Wire: com.squareup.retrofit2:converter-wire
  • Simple XML: com.squareup.retrofit2:converter-simplexml
  • JAXB: com.squareup.retrofit2:converter-jaxb
  • Scalars (primitives, boxed, and String): com.squareup.retrofit2:converter-scalars

聲明請(qǐng)求接口

interface MainPageApi{
  @GET("app_interface/home_pag/")
  fun getMainPageInfoWithRow():Call<MainPageInfo>
}

創(chuàng)建 Retrofit 對(duì)象

val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

發(fā)送請(qǐng)求

val mainPageApi = retrofit.create(MainPageApi::class.java)
mainPageApi.getMainPageInfoWithCall().enqueue(object:retrofit2.Callback<MainPageInfo>{
    override fun onResponse(
        call: Call<MainPageInfo>,
        response: retrofit2.Response<MainPageInfo>
    ) {
        Log.e("KotlinActivity","getMainPageInfoWithCall onResponse")
    }

    override fun onFailure(call: Call<MainPageInfo>, t: Throwable) {
        Log.e("KotlinActivity","getMainPageInfoWithCall onFailure")
    }
})

到這里為止,我們還沒有使用任何協(xié)程相關(guān)的特性,并且沒有都得寫回調(diào),和 Java 寫起來也沒啥差別。

支持協(xié)程

我們對(duì)接口的聲明加上suspend修飾

@GET("app_interface/home_pag/")
suspend fun getMainPageInfoWithRow():Call<MainPageInfo>

這時(shí)候上面直接發(fā)送請(qǐng)求的代碼會(huì)報(bào)錯(cuò): [圖片上傳失敗...(image-ba426c-1714897564066)] 提示我們需要在協(xié)程中調(diào)用,這也簡單,kotlin 對(duì) activity 有個(gè)擴(kuò)展的lifecycleScope成員變量,稍微修改一下:

lifecycleScope.launch(Dispatchers.IO) {
  mainPageApi.getMainPageInfoWithCall().enqueue(.....)
}

不習(xí)慣這么寫的話,可以將網(wǎng)絡(luò)請(qǐng)求寫在 ViewModel 中,通過 LiveData創(chuàng)建一個(gè)可觀察對(duì)象實(shí)現(xiàn)數(shù)據(jù)綁定。

不出意外的出意外了,應(yīng)用崩潰,錯(cuò)誤信息

java.lang.IllegalArgumentException: Suspend functions should not return Call, as they already execute asynchronously.
Change its return type to class com.huangyuanlove.androidtest.kotlin.retrofit.MainPageInfo

意思是在協(xié)程中發(fā)起請(qǐng)求已經(jīng)是異步的了,不需要再返回 Call 對(duì)象了,直接返回對(duì)應(yīng)的實(shí)體即可。 簡單,修改一下接口聲明

@GET("app_interface/home_page/")
suspend fun getMainPageInfoWithRow():MainPageInfo

然后修改一下請(qǐng)求

lifecycleScope.launch(Dispatchers.IO) {
  val mainPageInfo = mainPageApi.getMainPageInfo()
  withContext(Dispatchers.Main) {
    refreshUI(mainPageInfo)
  }
}

運(yùn)行一下,一切正常。我們修改一下接口,請(qǐng)求一個(gè)不存在的地址,會(huì)返回404,不出意外,應(yīng)用還是崩潰

retrofit2.HttpException: HTTP 404 
at retrofit2.KotlinExtensions$await$2$2.onResponse(KotlinExtensions.kt:53)
at retrofit2.OkHttpCall$1.onResponse(OkHttpCall.java:164)
at okhttp3.internal.connection.RealCall$AsyncCall.run(RealCall.kt:519)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1167)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:641)
at java.lang.Thread.run(Thread.java:929)
Suppressed: kotlinx.coroutines.internal.DiagnosticCoroutineContextException: [StandaloneCoroutine{Cancelling}@ffa6ad2, Dispatchers.IO]

哦~異常沒有處理,粗暴點(diǎn),直接 try-catch,kotlin 中還有runCatching這個(gè)語法糖

val mainPageInfoRow = runCatching { mainPageApi.getMainPageInfoWithRow() }
if (mainPageInfoRow.isFailure) {
    ToastUtils.showToast("請(qǐng)求失敗")
} else if (mainPageInfoRow.isSuccess) {
    ToastUtils.showToast("請(qǐng)求成功")
    withContext(Dispatchers.Main) {
        if (mainPageInfoRow.getOrNull() == null) {
            ToastUtils.showToast("請(qǐng)求結(jié)果為空")
        } else {
            refreshViewWithLaunch(mainPageInfoRow.getOrNull()!!)
        }

    }
}

但是有時(shí)候我們會(huì)用HTTP狀態(tài)碼來表示一些業(yè)務(wù)上邏輯錯(cuò)誤,并且不同的狀態(tài)碼返回的 JSON 結(jié)構(gòu)還可能不一樣。 別問為啥要這么搞,應(yīng)該是HTTP 狀態(tài)碼就應(yīng)該表示網(wǎng)絡(luò)請(qǐng)求的狀態(tài),業(yè)務(wù)狀態(tài)應(yīng)該放在返回的數(shù)據(jù)中約定字段來處理。問就是15年的老代碼,之前就是這么搞的,并且大范圍應(yīng)用,涉及到的部門、業(yè)務(wù)占半數(shù)以上。 這時(shí)候我們需要自定義CallAdapter

自定義 CallAdapter

這時(shí)候就應(yīng)該翻一下源碼了,在example有個(gè)ErrorHandlingAdapter.java,路徑在samples/src/main/java/com/example/retrofit/ErrorHandlingAdapter.java。 我們來仿寫一下,最關(guān)鍵的點(diǎn)在實(shí)現(xiàn)自己的 Call 類的時(shí)候,對(duì)callback 的處理。

定義不同的返回狀態(tài)

第一步,創(chuàng)建密閉類,來表示不同的狀態(tài),這里暫且定義了三種情況

  • Success:HTTP狀態(tài)碼在[200,300)這個(gè)區(qū)間
  • NetError:HTTP狀態(tài)碼不在[200,300)這個(gè)區(qū)間
  • UnknownError:其他錯(cuò)誤

sealed class NetworkResponse<out T : Any, out U : Any> { data class Success(val body: T) : NetworkResponse<T, Nothing>() data class NetError(val httpCode:Int?,val errorMsg:String?,val exception: Throwable?) : NetworkResponse<Nothing, Nothing>() data class UnknownError(val error: Throwable?) : NetworkResponse<Nothing, Nothing>() }

創(chuàng)建自己的Call類

這里為了簡化方便,除了enqueue之外必須重寫的方法,都是直接調(diào)用delegate對(duì)應(yīng)的方法

internal class NetworkResponseCall<S : Any, E : Any>(
    private val delegate: Call<S>,
    private val errorConverter: Converter<ResponseBody, E>
) : Call<NetworkResponse<S, E>> {
    override fun clone(): Call<NetworkResponse<S, E>> {
        return NetworkResponseCall(delegate.clone(), errorConverter);
    }

    override fun execute(): Response<NetworkResponse<S, E>> {
        throw UnsupportedOperationException("NetworkResponseCall doesn't support execute")
    }

    override fun isExecuted(): Boolean {
        return delegate.isExecuted;
    }

    override fun cancel() {
        delegate.cancel()
    }

    override fun isCanceled(): Boolean {
        return delegate.isCanceled
    }

    override fun request(): Request {
        return delegate.request()
    }

    override fun timeout(): Timeout {
        return delegate.timeout();
    }
}

下面是關(guān)鍵的enqueue方法,在這里面,將所有的請(qǐng)求都用Response.success返回,不再走Response.error.并且根據(jù)不同的 HTTP 狀態(tài)碼,返回的數(shù)據(jù)等條件轉(zhuǎn)成一開始定義的密閉類。


override fun enqueue(callback: Callback<NetworkResponse<S, E>>) {
    return delegate.enqueue(object : Callback<S> {
        override fun onResponse(call: Call<S>, response: Response<S>) {
            val body = response.body()
            val code = response.code()
            val error = response.errorBody()

            if (response.isSuccessful) {
                if (body != null) {
                    callback.onResponse(
                        this@NetworkResponseCall,
                        Response.success(NetworkResponse.Success(body))
                    )
                } else {
                    
                    callback.onResponse(
                        this@NetworkResponseCall,
                        Response.success(NetworkResponse.UnknownError(null))
                    )
                }
            } else {
                val errorBody = when {
                    error == null -> null
                    error.contentLength() == 0L -> null
                    else -> NetworkResponse.NetError(code, error.toString(), null)
                }
                if (errorBody != null) {
                    callback.onResponse(
                        this@NetworkResponseCall,
                        Response.success(errorBody)
                    )
                } else {
                    callback.onResponse(
                        this@NetworkResponseCall,
                        Response.success(NetworkResponse.UnknownError(null))
                    )
                }
            }


        }

        override fun onFailure(call: Call<S>, t: Throwable) {
            val networkResponse = when (t) {
                is Exception -> NetworkResponse.NetError(null,null,t)
                else -> NetworkResponse.UnknownError(t)
            }
            callback.onResponse(this@NetworkResponseCall, Response.success(networkResponse))
        }

    })
}

創(chuàng)建 CallAdapter

class NetworkResponseAdapter<S : Any, E : Any>(
    private val successType: Type,
    private val errorBodyConverter: Converter<ResponseBody, E>
) : CallAdapter<S, Call<NetworkResponse<S, E>>> {

    override fun responseType(): Type = successType

    override fun adapt(call: Call<S>): Call<NetworkResponse<S, E>> {
        return NetworkResponseCall(call, errorBodyConverter)
    }
}

創(chuàng)建CallAdapterFactory

class  NetworkResponseAdapterFactory:CallAdapter.Factory(){
    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        // suspend functions wrap the response type in `Call`
        if(Call::class.java != getRawType(returnType)){
            return null
        }
        check(returnType is ParameterizedType){
            "return type must be parameterized as Call<NetworkResponse<<Foo>> or Call<NetworkResponse<out Foo>>"
        }
        // get the response type inside the `Call` type
        val responseType = getParameterUpperBound(0,returnType)
        // if the response type is not ApiResponse then we can't handle this type, so we return null
        if(getRawType(responseType) != NetworkResponse::class.java){
            return null
        }


        // the response type is ApiResponse and should be parameterized
        check(responseType is ParameterizedType) { "Response must be parameterized as NetworkResponse<Foo> or NetworkResponse<out Foo>" }

        val successBodyType = getParameterUpperBound(0, responseType)
        val errorBodyType = getParameterUpperBound(1, responseType)

        val errorBodyConverter =
            retrofit.nextResponseBodyConverter<Any>(null, errorBodyType, annotations)

        return NetworkResponseAdapter<Any, Any>(successBodyType, errorBodyConverter)
    }
}

構(gòu)建 Retrofit 實(shí)例時(shí)添加該 Factory

val retrofit = Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addCallAdapterFactory(NetworkResponseAdapterFactory())
    .addConverterFactory(GsonConverterFactory.create())
    .build()

使用typealias簡化返回類型(可選)

data class HttpError(val httpCode:Int,val errorMsg:String?,val exception: Throwable?)
// before
interface DemoApiService {
    suspend fun mainPageInfo(): NetworkResponse<MainPageInfo, HttpError>
}
// after
typealias GenericResponse<S> = NetworkResponse<S, HttpError>

interface ApiService {
    suspend fun mainPageInfo(): GenericResponse<MainPageInfo>
}

使用

在 Activity 中直接使用lifecycleScope啟動(dòng)協(xié)程。

lifecycleScope.launch(Dispatchers.IO) {
    Log.e("KotlinActivity", "lifecycleScope.launch -->>" + Thread.currentThread().name);
    val mainPageInfo = mainPageApi.getMainPageInfo()

    withContext(Dispatchers.Main) {
        Log.e(
            "KotlinActivity",
            "withContext(Dispatchers.Main) -->>" + Thread.currentThread().name
        );
        when(mainPageInfo){

            is NetworkResponse.NetError -> Log.e("KotlinActivity",
                "NetError->$mainPageInfo"
            )
            is NetworkResponse.Success ->  refreshViewWithLaunch(mainPageInfo.body)
            is NetworkResponse.UnknownError -> Log.e("KotlinActivity","UnknownError->" + mainPageInfo.error)
        }
    }
}

或者在 ViewModel 中借助 LiveData 將返回值轉(zhuǎn)化為可觀察對(duì)象

class MainPageInfoViewModel:ViewModel() {
    private val _mainPageInfo  = MutableLiveData<MainPageInfo>()
    val mainPageInfo: LiveData<MainPageInfo> get() = _mainPageInfo
    fun getMainPageInfo(){
        viewModelScope.launch(Dispatchers.IO){
            val result = mainPageApi.getMainPageInfo()
            withContext(Dispatchers.Main){
                when(result){
                    is NetworkResponse.NetError -> Log.e("MainPageInfoViewModel",
                        "NetError->$result"
                    )
                    is NetworkResponse.Success ->  _mainPageInfo.value =  result.body
                    is NetworkResponse.UnknownError -> Log.e("MainPageInfoViewModel","UnknownError->" + result.error)
                }

            }
        }
    }

}

在 Activity 中使用

mainPageInfoModel = ViewModelProvider(this).get(MainPageInfoViewModel::class.java)
mainPageInfoModel.mainPageInfo.observe(this, Observer {
    if (it != null) {
        Log.e("KotlinActivity", "viewmodel獲取結(jié)果成功")
        refreshViewWithViewModelResult(it);
    } else {
        Log.e("KotlinActivity", "viewmodel獲取結(jié)果為空")
    }
})
mainPageInfoModel.getMainPageInfo()

暫時(shí)先這樣吧,基本上夠用了


康康主頁有驚喜~

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,572評(píng)論 6 531
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,071評(píng)論 3 414
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,409評(píng)論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,569評(píng)論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 71,360評(píng)論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 54,895評(píng)論 1 321
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,979評(píng)論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,123評(píng)論 0 286
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 48,643評(píng)論 1 333
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,559評(píng)論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 42,742評(píng)論 1 369
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,250評(píng)論 5 356
  • 正文 年R本政府宣布,位于F島的核電站,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 43,981評(píng)論 3 346
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,363評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,622評(píng)論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 51,354評(píng)論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 47,707評(píng)論 2 370

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