Retrying Network Requests with Flow

April 8, 2021

IMG_1282.jpg

Almost every app needs to make a network call and that network call may fail at any given time so it’s essential to be able to programmatically retry it. Not every endpoint calls for the same retries rules though. This post talks about implementing an injectable (if you use DI) retry policy. You can have a default policy used by most endpoints and a set of custom ones for individual endpoints.

If you are working on an Android app these days, you are probably using Kotlin so let’s create our RetryPolicy in Kotlin.

What are the things we want to control when setting up a network retry policy for a given API call?

  • number of retries

  • delay (in milliseconds)

  • delay factor (for exponential backoff)

The first 2 are self-explanatory. The 3rd one is used to calculate an effective delay before making the next retry call. As we keep retrying, the delay before the next retry increases. It’s called exponential backoff. Let’s create an example of RetryPolicy with KDoc documentation:

/**
 * Retry policy with exponential backoff.
 *
 * delayFactor is used to multiply delayMillis to increase the delay for the next retry.
 *
 * For instance, given a policy with numRetries of 4, delayMillis of 400ms and delayFactor of 2:
 *  - first retry: effective delayMillis will be 400
 *  - second retry: effective delayMillis will be 800
 *  - third retry: effective delayMillis will be 1600
 *  - forth retry: effective delayMillis will be 3200
 *
 * If no exponential backoff is desired, set delayFactor to 1
 */
interface RetryPolicy {
    val numRetries: Long
    val delayMillis: Long
    val delayFactor: Long
}

data class DefaultRetryPolicy(
    override val numRetries: Long = 4,
    override val delayMillis: Long = 400,
    override val delayFactor: Long = 2
) : RetryPolicy

Now that you have a RetryPolicy interface (or abstract class), you can inject it the contract (not the implementation) into your repository, whenever needed:

class MyRepositoryImpl(
  private val api: MyApi,
  private val retryPolicy: RetryPolicy
) : MyRepository

And if you use Kotlin Coroutines and Flow, you can use your RetryPolicy like so:

override suspend fun fetchRepos(userId: String): Flow<Result<Repos>> {
  return flow {
    emit(Result.success(api.fetchRepos(userId))
  }.retryWithPolicy(retryPolicy)
    .catch { emit(Result.failure(it)) }
  }
}

Notice the .retryWithPolicy(retryPolicy). This is where all the magic happens. It’s an extension function we wrote. We try to emit a value using Flow and if the remote call fails, we retry using the retryPolicy.

Here is the code for this extension function. Whenever, the network call fails with an IOException, we retry after a specific delay (currentDelay * delayFactor)

fun <T> Flow<T>.retryWithPolicy(
    retryPolicy: RetryPolicy
): Flow<T> {
    var currentDelay = retryPolicy.delayMillis
    val delayFactor = retryPolicy.delayFactor
    return retryWhen { cause, attempt ->
        if (cause is IOException && attempt < retryPolicy.numRetries) {
            delay(currentDelay)
            currentDelay *= delayFactor
            return@retryWhen true
        } else {
            return@retryWhen false
        }
    }
}

Finally, you can create multiple retry policy implementations that conform to RetryPolicy contracts and inject using Dependency injection mechanism of choice as needed.

This results in a clean, consistent, unit-testable code that’s customizable based on your endpoint’s particular needs.

 
Previous
Previous

RecyclerView ConcatAdapter

Next
Next

Jetpack Cross-NavGraph Destinations