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

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

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

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

当然,如果所有重试都失败,自动重试不会阻止我们向用户显示某种“重试按钮”,也不会阻止我们实施其他可能的策略,例如对 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

猜你喜欢

转载自blog.csdn.net/u011897062/article/details/129683096
今日推荐