Retry Operator in Kotlin Flow
- Authors
- Name
- Amit Shekhar
- Published on
I am Amit Shekhar, Co-Founder @ Outcome School, 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.
Join Outcome School and get high paying tech job: Outcome School
Before we start, I would like to mention that, I have released a video playlist to help you crack the Android Interview: Check out Android Interview Questions and Answers.
In this blog, we will learn about the Retry Operator in Kotlin Flow.
This blog is a part of the series I have written on Flow API in Kotlin:
- Mastering Flow API in Kotlin
- Creating Flow Using Flow Builder in Kotlin
- Terminal Operators in Kotlin Flow
- Cold Flow vs Hot Flow
- StateFlow and SharedFlow
- Long-running tasks in parallel with Kotlin Flow
- Retry Operator in Kotlin Flow - YOU ARE HERE
- Retrofit with Kotlin Flow
- Room Database with Kotlin Flow
- Kotlin Flow Zip Operator for Parallel Multiple Network Calls
- Instant Search Using Kotlin Flow Operators
- Exception Handling in Kotlin Flow
- callbackFlow - Callback to Flow API in Kotlin
- Unit Testing ViewModel with Kotlin Flow and StateFlow
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>
And, we use this operator as below:
.retryWhen { cause, attempt ->
}
Here, we have two parameters as follows:
- cause: This
cause
isThrowable
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
}
}
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>
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) }
}
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 theLong.MAX_VALUE
. - If we do not pass the
predicate
, it will provide true.
For example, we can do as below:
.retry()
It will keep retrying until the task gets completed successfully.
For example, we can also do as below:
.retry(3)
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
}
}
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)
}
}
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
}
}
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
}
}
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
}
}
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 thecurrentDelay
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.
Prepare yourself for Android Interview: Android Interview Questions
That's it for now.
Thanks
Amit Shekhar
Co-Founder @ Outcome School
You can connect with me on:
Follow Outcome School on: