Retrofit与协程的接口请求处理

354 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

本文主要分享在实际使用中Retrofit+协程的结果处理的方式

问题

在使用Retrofit的协程接口时,总有一大堆重复且必要的代码。用过协程的朋友们都知道,协程的错误基本上都是使用抛出异常来实现的,Retrofit也不例外。一个普通的请求接口定义如下:

public interface MyApi{
    suspend fun getUsers():BaseResponse<List<User>>
}

(PS:BaseResponse为服务器固定格式的包装类)

调用该请求的代码如下:

suspend fun getUsers():List<User>{
    try{
        val response = myApi.getUsers() //①
        if(response.code == 1){//假设code为1为成功,其他为失败
            return response.data
        }else{
            return emptyList()
        }
    }catch(e:Exception){
        return emptyLise()
    }
}

上述代码中,绝大部分的代码都是重复性的,也就是说每当我们增加接口的时候,都需要CV重复的代码。其实说到底,只有实际调用MyApi具体方法的这个地方(即①)有所区别,其他的判断code以及try-catch代码块都是相同的。作为一个有追求的码农,我们当然不允许这样的情况出现!

封装方案

首当其冲的就是try-catch,那简单参考Result的做法,定义一个方法,接受一个代码块,并将该代码块放入try中执行。说干就干!

fun <T> launch(block:() -> T):T{
    return try{
        block()
    }catch(e:Exception){
        T() //①
    }
}

骚等!这个T可咋搞,这可不能直接创建啊!(即①)

好家伙!我不要这个泛型了,直接改成User的list算了!

这时候我同事看不下去了:“怎么能这样呢?又不是什么接口都是查User的!”

我:“我早就想到了,改成List不就完事了吗?”

同事:“去你的!敢情你所有接口都请求一个列表啊?”

我:“即使不是请求列表的接口我也给弄成列表,只是单元素的列表而已,取的时候取列表的第一个元素不就是结果了吗?”

同事:“……”

好吧以上纯属开玩笑。事到如今,为了能够保证这个方法返回的类一致,Result当仁不让地站了出来。

fun <T> launch(block:() -> T):Result<T>{
    return try{
        Result.success(block())
    }catch(e:Exception){
        Result.failure(e)
    }
}

八卦的同事又过来瞄了一眼,不屑地说:“try-catch加Result,不是早就有了吗?合体进化!出来吧!就决定是你了!runCatching……”

这时候我打断了同事的腿,呸!同事的话:“你还是太小看我了!我当然知道有runCatching,我不用是有自己的想法的。”

原本我就是使用Result配套的runCatching的,直到我遇到了某一个接口——上传文件。为了调试方便,Retrofit的日志打印等级一直是BODY级别。那就造成了当我在调试上传文件这个接口的时候,AS的Logcat被塞爆的情况。当然有很多方法解决这个问题,比如把日志的buffer调大等等,但我其实并不关心文件的内容,所以我选择把打印的等级调成HEADERS。

但是又出现了新的问题,在发起上传请求之前,手动把日志等级调成HEADERS,那我往后的其他需要关心内容的接口不也看不到了?

同事:“请求成功之后调回来就好啦!”

我:“那如果上传失败走到catch里面呢?”

同事:“那在catch里面也加上调日志的代码咯。”

我:“……”

讲到这里,下一位角色应该也呼之欲出了,它就是:finally。

不管有没有catch,我都得把日志等级调回去,这不正是final的使用场景吗?

奈何Result并没有配套finally相关的方法,这也是我放弃了runCatching的原因。

加上final的逻辑后,launch方法如下:

fun <T> launch(
        final: () -> Unit = {},
        block: () -> T
    ): Result<T> {
        return try {
            Result.success(block())
        } catch (e: Exception) {
            Result.failure(e)
        } finally {
            final()
        }
    }

launch方法接受一个无参无返回的方法final,以及一个无参返回T的方法block。final放在前面并且默认值为空方法,因为大部分接口都不需要使用final,因为Kotlin的语法特性,block在最后使得在使用上可以写成Lambda形式。

调用如下:

fun getUsers():Result<List<User>>{
    return launch{
        val response = myApi.getUsers()
        if(response.code == 1){
            return response.data
        }else{
            return emptyList
        }
    }
}

去掉了try-catch,接下来受刑的就是判断code了。code是BaseResponse的内在属性,判断放在其内部是非常合理的,于是乎:

private const val SUCCESS_CODE = 100000data class BaseResponse<T>(val code: Int, val message: String, val data: T) {
    fun getResult(): Result<T> {
        return if (code == SUCCESS_CODE) {
            Result.success(data)
        } else {
            Result.failure(Exception("$message:$code"))
        }
    }
}

launch方法进一步修改成如下代码:

suspend fun <T> launch(
        final: () -> Unit = {},
        block: suspend () -> BaseResponse<T>
    ): Result<T> {
        return try {
            block().getResult()
        } catch (e: Exception) {
            Result.failure(e)
        } finally {
            final()
        }
    }

block的返回值从T改成了BaseResponse,try里面调用BaseResponse的getResult方法获取结果。

因为使用了Retrofit的协程方法,所以block需要是挂起方法,同时launch也需要是挂起方法。由于Retrofit的协程调用已经在内部实现了线程切换,所以无需再手动切换线程了。

那么launch最终的调用就简化成这样了:

fun getUsers():Result<List<User>> = launch{ myApi.getUsers() }

如此这般,就将一开始繁复的网络请求简化成一行代码。而且使用Result来返回结果对于处理异常也非常方便。

RetrofitManager.getUsers()
    .onSuccess{}
    .onFailure{}

Result的设计也非常精妙,以后有时间解读一下。

疑问

虽然说这样封装很棒(至少我自己是这么觉得的),但在下载文件的情况下就无能为力了。Retrofit的下载文件需要在API中定义一个返回值为Response的方法。收到回复后调用body获取输入流,后续进行写入文件的操作。这其中并不涉及BaseResponse这个类,所以无法使用launch方法。有解决方法的朋友们欢迎评论指导一下哦。

最后

当然本文的做法只是浅浅地封装,更多的只是我在实际开发中的小技巧,不敢与其他真正封装Retrofit的大佬比肩。只要有人能有收获那就行了。最后当然欢迎友善交流,拒绝开喷!