While control flow is linear the advantages of coroutines may be not so clearly obvious. However things change when loops, conditions, try-catch blocks arise.
Coroutines make it easy to write complex control flows involving suspension points.
For example this code that executes suspend function several times until it completes successfully looks pretty straightforward:
suspend fun downloadImageWithRetry(retryCount: Int): Image {
repeat(retryCount - 1) {
try {
return downloadImage()
}
catch (e: IOException) {
// ignore or log failed trial
}
}
return downloadImage() // last one trial
}
suspend fun downloadImage(): Image = ... // download without retries
While it is possible to rewrite this control flow such that suspend fun(): Image
becomes fun (): CompletableFuture<Image>
, the result that requires to rewrite a loop to a recursion is not so easy to compose and even more not so easy to grasp later:
fun downloadImage(): CompletableFuture<Image> = TODO()
fun downloadImageWithRetries(retryCount: Int): CompletableFuture<Image> {
return downloadImage().handle { image: Image?, error: Throwable? ->
if (error != null) {
if (error is IOException && retryCount > 1)
// ignore or log
downloadImageWithRetries(retryCount - 1)
else
throw error
}
else {
CompletableFuture.completedFuture(image!!)
}
}.thenCompose { it }
}