linwoain的个人blog

知我者谓我心忧,不知我者谓我何求

0%

Retrofit+kotlin协程(coroutines)的安全且优雅用法

retrofit是现今流行的网络请求框架,现今有了kotlin协程的加持,如虎添翼,通常用法如下:
1、定义rest接口

1
2
3
4
5
6
interface Api {
@POST("login")
suspend fun loginByPassword(
@Body map: LoginWrapper
): Response<LoginBean>
}

2、定义请求包装类

1
data class LoginWrapper(@Keep val username: String, @Keep val password: String? = null)

与返回包装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
data class Response<out T>(
@Keep val code: Int?,
@Keep val msg: String?,
@Keep val data: T?
) {
val success get() = code == 200
}

data class LoginBean(
@Keep val realName: String = "",
@Keep val nickName: String = "",
@Keep val id: String = "",
@Keep val hiredate: Date? = null,
@Keep val token: String = ""
)

2、创建调用实例

1
2
3
4
5
6
7
8
9
10
11
val retrofit: Retrofit = Retrofit.Builder()
.baseUrl("https://api.linwoain.com")
.addConverterFactory(
GsonConverterFactory.create(
GsonBuilder().setDateFormat("yyyy-MM-dd HH:mm:ss").create()
)
)
.client(client)
.build()

val remoteService: PandoraApi = retrofit.create()

3、调用接口

1
2
3
4
5
6
7
8
9
lifecycleScope.launch {
val response =
remoteService.loginByPassword(LoginWrapper(username = "linwoain", password = "123456") )
if (response.success) {
name.text = response.data?.nickName
} else {
Toast.makeText(this@MainActivity, response.msg, Toast.LENGTH_SHORT).show()
}
}

一切都很美好,无论成功还是失败都能正常返回,但是如果请求的时候超时了;

app直接崩溃闪退了,不仅是超时,如果有断网,或者返回json格式不正确都会导致崩溃,我原来用rxjava也没这问题呀。
究其原因 我们根本没有对这些异常进行捕获,而rxjava是已经帮我们做了。那么我们怎么处理这个异常呢?
一种方法是调用的时候对异常显式的捕获,简单的说就是catch住,方法很简单,但是每次调用都catch一下,如果接口里方法很多。
这种方法就显得繁琐而不优雅。如果要对一个类的所以方法做个处理,那不就是切面编程吗!而且这是一个接口,天然就可以动态代理
我们将所有api接口方法统一catch住,如果发生异常。返回一个统一的错误实体

1
2
3
4
5
6
7
8
val delegateRemoteService = Proxy.newProxyInstance(
Api::class.java.classLoader, arrayOf(Api::class.java)
) { _, method, args ->
val response = runCatching { method.invoke(remoteService, *args) }.getOrDefault(
PandoraResponse(404, "网络请求错误", null)
)
response
} as Api

将调用处remoteService改为delegateRemoteService,试一下发现还会有问题。debug会发现这块儿catch住还在调用请求的线程,而异常发生在子线程中。所以统一trycatch这条路还是不通。
这时候,就想起来线程的切换是由kotlin协程来管理的,反编译下kotlin代码,发现其在每一个suspend修饰的接口方法里,会传递一个
Continuation对象,

1
2
3
4
5
6
7
8
9
10
11
12
@Metadata(
mv = {1, 1, 18},
bv = {1, 0, 3},
k = 1,
d1 = {"\u0000\u001c\n\u0002\u0018\u0002\n\u0002\u0010\u0000\n\u0000\n\u0002\u0018\u0002\n\u0002\u0018\u0002\n\u0000\n\u0002\u0018\u0002\n\u0002\b\u0002\bf\u0018\u00002\u00020\u0001J!\u0010\u0002\u001a\b\u0012\u0004\u0012\u00020\u00040\u00032\b\b\u0001\u0010\u0005\u001a\u00020\u0006H§@ø\u0001\u0000¢\u0006\u0002\u0010\u0007\u0082\u0002\u0004\n\u0002\b\u0019¨\u0006\b"},
d2 = {"Lcom/linwoain/myapplication/PandoraApi;", "", "loginByPassword", "Lcom/linwoain/myapplication/PandoraResponse;", "Lcom/linwoain/myapplication/LoginBean;", "map", "Lcom/linwoain/myapplication/LoginWrapper;", "(Lcom/linwoain/myapplication/LoginWrapper;Lkotlin/coroutines/Continuation;)Ljava/lang/Object;", "app_debug"}
)
public interface PandoraApi {
@POST("login")
@Nullable
Object loginByPassword(@Body @NotNull LoginWrapper var1, @NotNull Continuation var2);
}

这是一个接口,定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
public interface Continuation<in T> {
/**
* The context of the coroutine that corresponds to this continuation.
*/
public val context: CoroutineContext

/**
* Resumes the execution of the corresponding coroutine passing a successful or failed [result] as the
* return value of the last suspension point.
*/
public fun resumeWith(result: Result<T>)
}

我称其为恢复器,其中的resumeWith方法就是恢复时的回调,可以在这个地方处理掉可能的异常。我们可以用kotlin里静态代理的便宜写法,替换掉默认恢复器

@Suppress("UNCHECKED_CAST")
val delegateRemoteService = Proxy.newProxyInstance(
    PandoraApi::class.java.classLoader, arrayOf(PandoraApi::class.java)
) { _, method, args ->
    val last = args.last()
    if (last is Continuation<*>) {
        args[args.lastIndex] = DelegateContinuation(last as Continuation<Any>)
    }
    method.invoke(remoteService, *args)
} as PandoraApi

class DelegateContinuation(
    private val continuation: Continuation<Any>
) : Continuation<Any> by continuation {
    companion object{
        private val error by lazy {  Response(-1, "网络错误", null) }

    }
    override fun resumeWith(result: Result<Any>) {
        GlobalScope.launch(context) {
            if (result.isFailure) {
                continuation.resumeWith(Result.success(error))
            } else {
                result.getOrNull()?.also {
                    if (it is Response<*>) {

                        continuation.resumeWith(result)
                    }
                }
            }
        }

    }

}

这里将所有的异常都当作了网络错误,想细分的话可以调用result的exceptionOrNull(),拿到具体的异常具体处理。这样在接口调用处拿到的就一定是一个Response对象。同时这里也是除了okhttp的interceptor之外可以对网络请求统一处理的地方,如token刷新,失效时重新请求。对失败请求弹个toast等等