如何在 Android + Kotlin 中自动重试网络请求
迪丽瓦拉
2025-06-01 00:40:36
0

如何在 Android + Kotlin 中自动重试网络请求

2023 年的三种流行方式:RxJava / Coroutines / OkHttp

article loading
应用的一种常见业务是错误处理,网络重试便是要处理的业务之一,当网络状况不理想时,这种状况就会出现。

当然,如果所有重试都失败,自动重试不会阻止我们向用户显示某种“重试按钮”,也不会阻止我们实施其他可能的策略,例如对 Internet 可用性做出反应。但让我们关注本文中的第一个选项。

解决方案要求

作为开发人员,我们可能需要什么样的解决方案?

  • 便于使用。一行包装器是理想的。甚至网络层的全局配置。

  • 可定制。我们可能希望针对不同的错误实施不同的重试策略。

  • 适合我们的技术堆栈。当然,我们不想仅仅因为特定的解决方案就从 RxJava 迁移到协程,反之亦然。
    在这篇文章中,我将分享如何实现重试:

  • RxJava

  • Kotlin 协程

  • OkHttp 拦截器

RxJava

将一切数据看作流,甚至异常也作为一种流进行处理。
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()

Kotlin Coroutines

我们可以使用和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()
}

OkHttp interceptors

前面的解决方案很灵活,支持多种参数。此外,它们不仅可以用于网络,还可以用于任何类型的操作或计算。

另一方面,有些人可能会忘记将 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)}
}

如果出现以下情况,我们会延迟重试:

  • 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

相关内容