DEV Community

Cover image for Retry Operator in Kotlin Flow
Amit Shekhar
Amit Shekhar

Posted on • Edited on • Originally published at outcomeschool.com

Retry Operator in Kotlin Flow

Hi, I am Amit Shekhar, Co-Founder @ Outcome School • IIT 2010-14 • I have taught and mentored many developers, and their efforts landed them high-paying tech jobs, helped many tech companies in solving their unique problems, and created many open-source libraries being used by top companies. I am passionate about sharing knowledge through open-source, blogs, and videos.

In this blog, we will learn about the Retry Operator in Kotlin Flow.

This article was originally published at Outcome School.

I will be using the following project for the implementation part. The project follows a basic MVVM Architecture for simplicity. You can find the complete code for the implementation mentioned in this blog in the project itself.

GitHub Project: Learn Kotlin Flow

When we talk about retrying a task using operators in Kotlin Flow, we talk about the following two operators:

  • retryWhen
  • retry

Both operators can be used interchangeably in most cases, we will learn about them today.

retryWhen

Let's look at the source code to understand the definition of the retryWhen operator.

fun <T> Flow<T>.retryWhen(predicate: suspend FlowCollector<T>.(cause: Throwable, attempt: Long) -> Boolean): Flow<T>
Enter fullscreen mode Exit fullscreen mode

And, we use this operator as below:

.retryWhen { cause, attempt ->

}
Enter fullscreen mode Exit fullscreen mode

Here, we have two parameters as follows:

  • cause: This cause is Throwable which is the base class for all errors and exceptions.
  • attempt: This attempt is the number that represents the current attempt. It starts with zero.

For example, if there is an exception when we started the task, we will receive the cause(exception) and attempt(0).

The retryWhen takes a predicate function to decide whether to retry or not.

If the predicate function returns true, then only it will retry else it will not.

For example, we can do as below:

.retryWhen { cause, attempt ->
    if (cause is IOException && attempt < 3) {
        delay(2000)
        return@retryWhen true
    } else {
        return@retryWhen false
    }
}
Enter fullscreen mode Exit fullscreen mode

In this case, we are returning true when the cause is IOException, and the attempt count is less than 3.

So, it will only retry if the condition is satisfied.

Note: As the predicate function is suspending function, we can call another suspending function from it.

If we notice in the above code, we have called the delay(2000), so that it retries only after a delay of 2 seconds.

retry

This is the definition of the retry Flow operator.

fun <T> Flow<T>.retry(
    retries: Long = Long.MAX_VALUE,
    predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T>
Enter fullscreen mode Exit fullscreen mode

The complete block from the source code of Kotlin Flow.

fun <T> Flow<T>.retry(
    retries: Long = Long.MAX_VALUE,
    predicate: suspend (cause: Throwable) -> Boolean = { true }
): Flow<T> {
    require(retries > 0) { "Expected positive amount of retries, but had $retries" }
    return retryWhen { cause, attempt -> attempt < retries && predicate(cause) }
}
Enter fullscreen mode Exit fullscreen mode

If we see the retry function, it actually calls the retryWhen internally.

retry function has default arguments.

  • If we do not pass the retries, it will use the Long.MAX_VALUE.
  • If we do not pass the predicate, it will provide true.

For example, we can do as below:

.retry()
Enter fullscreen mode Exit fullscreen mode

It will keep retrying until the task gets completed successfully.

For example, we can also do as below:

.retry(3)
Enter fullscreen mode Exit fullscreen mode

It will only retry 3 times.

For example, we can also do as below:

.retry(retries = 3) { cause ->
    if (cause is IOException) {
        delay(2000)
        return@retry true
    } else {
        return@retry false
    }
}
Enter fullscreen mode Exit fullscreen mode

Here, it becomes very similar to what we did using the retryWhen above.

Here, we are returning true when the cause is IOException. So, it will only retry when the cause is IOException.

If we notice in the above code, we have called the delay(2000), so that it retries only after a delay of 2 seconds.

Now, let's see the code examples.

This is a function to simulate a long-running task with exceptions.

private fun doLongRunningTask(): Flow<Int> {
    return flow {
        // your code for doing a long running task
        // Added delay, random number, and exception to simulate
        delay(2000)
        val randomNumber = (0..2).random()
        if (randomNumber == 0) {
            throw IOException()
        } else if (randomNumber == 1) {
            throw IndexOutOfBoundsException()
        }
        delay(2000)
        emit(0)
    }
}
Enter fullscreen mode Exit fullscreen mode

Now, when using the retry operator

viewModelScope.launch {
    doLongRunningTask()
        .flowOn(Dispatchers.Default)
        .retry(retries = 3) { cause ->
            if (cause is IOException) {
                delay(2000)
                return@retry true
            } else {
                return@retry false
            }
        }
        .catch {
           // error
        }
        .collect {
            // success
        }
}
Enter fullscreen mode Exit fullscreen mode

Similarly, when using the retryWhen operator

viewModelScope.launch {
    doLongRunningTask()
        .flowOn(Dispatchers.Default)
        .retryWhen { cause, attempt ->
            if (cause is IOException && attempt < 3) {
                delay(2000)
                return@retryWhen true
            } else {
                return@retryWhen false
            }
        }
        .catch {
            // error
        }
        .collect {
            // success
        }
}
Enter fullscreen mode Exit fullscreen mode

If we see, every time we are adding the delay of 2 seconds, but in real use-cases, we add delay with exponential backoff. Do not worry, we will implement that too.

Retry Operator with Exponential Backoff Delay

After adding the code for the delay with exponential backoff

viewModelScope.launch {
    var currentDelay = 1000L
    val delayFactor = 2
    doLongRunningTask()
        .flowOn(Dispatchers.Default)
        .retry(retries = 3) { cause ->
            if (cause is IOException) {
                delay(currentDelay)
                currentDelay = (currentDelay * delayFactor)
                return@retry true
            } else {
                return@retry false
            }
        }
        .catch {
            // error
        }
        .collect {
            // success
        }
}
Enter fullscreen mode Exit fullscreen mode

Here, we have created two variables:

  • currentDelay: This represents the delay to be used in the current retry.
  • delayFactor: We use this delayFactor to multiply it with the currentDelay to increase the delay for the next retry.

That's it, we have implemented the retry with exponential backoff delay.

You can build, run, and play all the examples in the project provided.

This way we can use retry and retryWhen Operators of Flow to solve the interesting problem in Android App Development. Remember both can be used interchangeably in most cases that we solve in Android App Development.

Master Kotlin Coroutines from here: Mastering Kotlin Coroutines

That's it for now.

Thanks

Amit Shekhar

Co-Founder @ Outcome School

You can connect with me on:

Read all of our blogs here.

Top comments (0)