如何在 Android + Kotlin 中自动重试网络请求
2023 年的三种流行方式:RxJava / Coroutines / OkHttp
应用的一种常见业务是错误处理,网络重试便是要处理的业务之一,当网络状况不理想时,这种状况就会出现。
当然,如果所有重试都失败,自动重试不会阻止我们向用户显示某种“重试按钮”,也不会阻止我们实施其他可能的策略,例如对 Internet 可用性做出反应。但让我们关注本文中的第一个选项。
解决方案要求
作为开发人员,我们可能需要什么样的解决方案?
-
便于使用。一行包装器是理想的。甚至网络层的全局配置。
-
可定制。我们可能希望针对不同的错误实施不同的重试策略。
-
适合我们的技术堆栈。当然,我们不想仅仅因为特定的解决方案就从 RxJava 迁移到协程,反之亦然。
在这篇文章中,我将分享如何实现重试: -
RxJava
-
Kotlin 协程
-
OkHttp 拦截器
RxJava
将一切数据看作流,甚至异常也作为一种流进行处理。
Rxjava中有许多方法可以实现重试功能,但这里我们使用retryWhen()
操作符,它比repeate
操作符更灵活,比自定义Observable
容易。
主要的思路是:只有我们想将错误向下传递时我们才使用map
操作符将其映射为Observable.error
。
下面的代码可以满足大部分场景的重试需求:
fun <T : Any> Observable<T>.withRetrying(
fallbackValue: T?,
tryCnt: Int,
intervalMillis: (attempt: Int) -> Long,
retryCheck: (Throwable) -> Boolean,
): Observable<T>
fallbackValue
—如果所有重试都以失败告终,则发出该值。如果我们准备好处理下游某处的错误,则为 null。tryCnt
—是我们将尝试请求和重新请求的总次数。intervalMillis
— 是一个 lambda,我们可以在其中实现增加的延迟。retryCheck
— 是一个 lambda,我们可以在其中决定是否需要重试此特定错误。通常,网络错误和 5xx HTTP 代码会重试,但 4xx 代码则不会。
其实现如下:
fun <T : Any> Observable<T>.withRetrying(
fallbackValue: T?,
tryCnt: Int,
intervalMillis: (attempt: Int) -> Long,
retryCheck: (Throwable) -> Boolean,
): Observable<T> {
if (tryCnt <= 0) {
return this
}
return this
.retryWhen {
errors ->
errors
.zipWith(
Observable.range(1, tryCnt)
) {
th: Throwable, attempt: Int ->
if (retryCheck(th) && attempt < tryCnt) {
Observable.timer(intervalMillis(attempt), TimeUnit.MILLISECONDS)
} else {
Observable.error(th)
}
}
.flatMap {
it }
}
.let {
if (fallbackValue == null) {
it
} else {
it.onErrorResumeNext {
Observable.just(fallbackValue) }
}
}
}
为Single
及其他类型流包装的函数如下:
fun <T : Any> Single<T>.withRetrying(
fallbackValue: T?,
tryCnt: Int,
intervalMillis: (attempt: Int) -> Long,
retryCheck: (Throwable) -> Boolean,
): Single<T> = this
.toObservable()
.withRetrying(fallbackValue, tryCnt, intervalMillis, retryCheck)
.firstOrError()
为了进一步简化,它可以包装为项目通用功能。例如,如果您的常用策略是重试 3 次并增加延迟,那么它将是这样的:
fun <T : Any> Single<T>.commonRetrying(fallbackValue: T? = null) =
withRetrying(fallbackValue, 3, {
2000L * it }, networkRetryCheck)
private val networkRetryCheck: (Throwable) -> Boolean = {
val shouldRetry = when {
it.isHttp4xx() -> false
else -> true
}
shouldRetry
}
最终的示例代码
在你的数据层,只需要多增加一行代码commonRetrying()
如下:
fun getSomething(params: String): Single<YourResponseType> =
api.getSomething(params)
.commonRetrying()
Kotlin Coroutines
我们可以使用和RxJava实现中相同的接口和参数,代码如下:
suspend fun <T> retrying(
fallbackValue: T?,
tryCnt: Int,
intervalMillis: (attempt: Int) -> Long,
retryCheck: (Throwable) -> Boolean,
block: suspend () -> T,
): T {
try {
val retryCnt = tryCnt - 1
repeat(retryCnt) {
attempt ->
try {
return block()
} catch (e: Exception) {
if (e is CancellationException || !retryCheck(e)) {
throw e
}
}
delay(intervalMillis(attempt + 1))
}
return block()
} catch (e: Exception) {
if (e is CancellationException) {
throw e
}
return fallbackValue ?: throw e
}
}
算法很简单:
retryCnt
在循环中尝试多次,在循环之后再尝试一次。- 检查
retryCheck
是否需要在特定异常后重试。如果是,则在下一次尝试之前延迟intervalMillis
。 - 如果所有尝试都失败但我们有一个
fallbackValue
返回 - 返回它。否则,进一步抛出错误。
同样,我们可以参照上面RxJava的做法,将其提取到工程的特定位置作为公有方法:
suspend fun <T> commonRetrying(
fallbackValue: T?,
block: suspend () -> T,
): T = retrying(fallbackValue, 3, {
2000L * it }, networkRetryCheck, block)
最终调用的示例代码如下:
suspend fun getSomething() = commonRetrying {
api.getSomething()
}
OkHttp interceptors
前面的解决方案很灵活,支持多种参数。此外,它们不仅可以用于网络,还可以用于任何类型的操作或计算。
另一方面,有些人可能会忘记将 API 调用包装到此类函数中。在这种情况下,我们可以选择将重试逻辑实现到网络层,特别是 OkHttp。但与前面的例子相比,它有一些局限性。仅针对特定请求应用特定的重试策略更加困难。此外,如果在网络调用和调用端之间的某处发生错误,例如在响应数据解析阶段,它也不会重试。是好是坏——这取决于您项目的需求。
基本实现如下所示。它不包含对 4xx 和 5xx HTTP 代码的检查,但它也可以实现。
import okhttp3.Interceptor
import okhttp3.Response
class RetryingInterceptor : Interceptor {
private val tryCnt = 3
private val baseInterval = 2000L
override fun intercept(chain: Interceptor.Chain): Response {
return process(chain, attempt = 1)
}
private fun process(chain: Interceptor.Chain, attempt: Int): Response {
var response: Response? = null
try {
val request = chain.request()
response = chain.proceed(request)
if (attempt < tryCnt && !response.isSuccessful) {
return delayedAttempt(chain, response, attempt)
}
return response
} catch (e: Exception) {
if (attempt < tryCnt && networkRetryCheck(e)) {
return delayedAttempt(chain, response, attempt)
}
throw e
}
}
private fun delayedAttempt(
chain: Interceptor.Chain,
response: Response?,
attempt: Int,
): Response {
response?.body?.close()
Thread.sleep(baseInterval * attempt)
return process(chain, attempt = attempt + 1)
}
}
如果出现以下情况,我们会延迟重试:
- a)检查
isSuccessful
失败 - b)发生异常
如果
chain.proceed(request)
被多次调用,则必须关闭先前的响应主体。
通过以下方式注入拦截器:
val client = OkHttpClient.Builder()
.addInterceptor(RetryingInterceptor())
.build()
结论
我们给出了3种解决网络重试的方案,在实际项目中你需要视具体情况来选择,但是有如下建议:如果您需要为整个应用程序使用单一的重试策略——OkHttp 拦截器是一个合理的选择。如果您需要对特定请求进行更多控制,或者您需要重试一些不在后台使用 OkHttp 的东西——决定取决于您的技术栈。现在,通常是 RxJava 或 Kotlin Coroutines。它们都足够灵活来完成这项任务。
参考
https://medium.com/mobilepeople/how-to-retry-network-requests-automatically-in-android-kotlin-64dcafb7f294