2023 年的三种流行方式:RxJava / Coroutines / OkHttp
应用的一种常见业务是错误处理,网络重试便是要处理的业务之一,当网络状况不理想时,这种状况就会出现。
当然,如果所有重试都失败,自动重试不会阻止我们向用户显示某种“重试按钮”,也不会阻止我们实施其他可能的策略,例如对 Internet 可用性做出反应。但让我们关注本文中的第一个选项。
作为开发人员,我们可能需要什么样的解决方案?
便于使用。一行包装器是理想的。甚至网络层的全局配置。
可定制。我们可能希望针对不同的错误实施不同的重试策略。
适合我们的技术堆栈。当然,我们不想仅仅因为特定的解决方案就从 RxJava 迁移到协程,反之亦然。
在这篇文章中,我将分享如何实现重试:
RxJava
Kotlin 协程
OkHttp 拦截器
将一切数据看作流,甚至异常也作为一种流进行处理。
Rxjava中有许多方法可以实现重试功能,但这里我们使用retryWhen()
操作符,它比repeate
操作符更灵活,比自定义Observable
容易。
主要的思路是:只有我们想将错误向下传递时我们才使用map
操作符将其映射为Observable.error
。
下面的代码可以满足大部分场景的重试需求:
fun Observable.withRetrying(fallbackValue: T?,tryCnt: Int,intervalMillis: (attempt: Int) -> Long,retryCheck: (Throwable) -> Boolean,
): Observable
fallbackValue
—如果所有重试都以失败告终,则发出该值。如果我们准备好处理下游某处的错误,则为 null。tryCnt
—是我们将尝试请求和重新请求的总次数。intervalMillis
— 是一个 lambda,我们可以在其中实现增加的延迟。retryCheck
— 是一个 lambda,我们可以在其中决定是否需要重试此特定错误。通常,网络错误和 5xx HTTP 代码会重试,但 4xx 代码则不会。其实现如下:
fun Observable.withRetrying(fallbackValue: T?,tryCnt: Int,intervalMillis: (attempt: Int) -> Long,retryCheck: (Throwable) -> Boolean,
): Observable {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 Single.withRetrying(fallbackValue: T?,tryCnt: Int,intervalMillis: (attempt: Int) -> Long,retryCheck: (Throwable) -> Boolean,
): Single = this.toObservable().withRetrying(fallbackValue, tryCnt, intervalMillis, retryCheck).firstOrError()
为了进一步简化,它可以包装为项目通用功能。例如,如果您的常用策略是重试 3 次并增加延迟,那么它将是这样的:
fun Single.commonRetrying(fallbackValue: T? = null) =withRetrying(fallbackValue, 3, { 2000L * it }, networkRetryCheck)private val networkRetryCheck: (Throwable) -> Boolean = {val shouldRetry = when {it.isHttp4xx() -> falseelse -> true}shouldRetry
}
最终的示例代码
在你的数据层,只需要多增加一行代码commonRetrying()
如下:
fun getSomething(params: String): Single =api.getSomething(params).commonRetrying()
我们可以使用和RxJava实现中相同的接口和参数,代码如下:
suspend fun retrying(fallbackValue: T?,tryCnt: Int,intervalMillis: (attempt: Int) -> Long,retryCheck: (Throwable) -> Boolean,block: suspend () -> T,
): T {try {val retryCnt = tryCnt - 1repeat(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 commonRetrying(fallbackValue: T?,block: suspend () -> T,
): T = retrying(fallbackValue, 3, { 2000L * it }, networkRetryCheck, block)
最终调用的示例代码如下:
suspend fun getSomething() = commonRetrying {api.getSomething()
}
前面的解决方案很灵活,支持多种参数。此外,它们不仅可以用于网络,还可以用于任何类型的操作或计算。
另一方面,有些人可能会忘记将 API 调用包装到此类函数中。在这种情况下,我们可以选择将重试逻辑实现到网络层,特别是 OkHttp。但与前面的例子相比,它有一些局限性。仅针对特定请求应用特定的重试策略更加困难。此外,如果在网络调用和调用端之间的某处发生错误,例如在响应数据解析阶段,它也不会重试。是好是坏——这取决于您项目的需求。
基本实现如下所示。它不包含对 4xx 和 5xx HTTP 代码的检查,但它也可以实现。
import okhttp3.Interceptor
import okhttp3.Responseclass RetryingInterceptor : Interceptor {private val tryCnt = 3private val baseInterval = 2000Loverride fun intercept(chain: Interceptor.Chain): Response {return process(chain, attempt = 1)}private fun process(chain: Interceptor.Chain, attempt: Int): Response {var response: Response? = nulltry {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)}
}
如果出现以下情况,我们会延迟重试:
isSuccessful
失败如果
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