What callback-hell is and how to get rid of callbacks with Tasks API
Have you ever been in a callback-hell? I have been, and I don’t want to go back there.
In this article we will lean into the so-called “callback-hell” and how we can simplify our lives with the Tasks API. You’ve probably at least once encountered operations that take some time — they can’t be executed in the main thread because they block the UI, so you have to wait for feedback. You don’t know how long it will take and whether it will succeed at all. If it succeeds then you want to continue the calculation, and if it doesn’t then show the user an error. Of course, the simplest solution is to pass to the function, another function, which will execute when you finish the calculation successfully. Unfortunately, such a solution can push us into an endless string of nested calls and spoil the readability of our code. Finding an error under such circumstances borders on the miraculous.
And I also fell into this trap one day ;(
Let’s see with a simple example what a callback-hell is:
fun computeSomething(onSuccess: (Int) -> Unit,
onError: (String) -> Unit) {
// Imitate heavy computation
Thread.sleep(3000) // 3 seconds
val isSuccess = Random.nextBoolean() // Simulate success/error
if (isSuccess) onSuccess(1) // Hurray! We computed some value!
else onError("Something wrong!") // :(
}
fun computeSomethingAgain(
veryImportantValue: Int,
onSuccess: (Int) -> Unit,
onError: (String) -> Unit
) {
if (veryImportantValue in 0..10) {
// Imitate heavy computation
Thread.sleep(3000)
val isSuccess = Random.nextBoolean()
if (isSuccess) onSuccess(2)
else onError("Something goes wrong")
} else {
// If previous computation return wrong number return error
onError("Your value is invalid!")
}
}
And now take a look at this nasty nested code (remember, this is ONLY a small example, and in a real, extended program it could be much worse)
// Ugly nested callbacks :( It can go infinite
computeSomething(onSuccess = { value ->
// If success call another function
computeSomethingAgain(
veryImportantValue = value,
onSuccess = { finishValue ->
// Next functions...infinite loop..
},
onError = { errorMsg2 ->
// Handle errors
})
}, onError = { errorMsg1 ->
// Handle errors
})
You already know what callback-hell is, now I will share my adventure with ML Kit. This was the moment when I realized that using too many callbacks can indeed become hell.
The other day while working with ML Kit I encountered a strange error…. I was waiting for feedback whether I could pass the next frame, but none of the available callbacks (success ,failure, complete and cancel) were called. This was happening unpredictably, on some phones only.
Sample code from Barcode Scanner to illustrate what it more or less looks like if you’ve never used ML Kit:
scanner.process(image)
.addOnSuccessListener { barcodes ->
// Task completed successfully
}
.addOnFailureListener {
// Task failed with an exception
}
... // and others callbacks
And then I started looking for a solution how to get out of this situation how to get rid of these callbacks and fix the issue. I came across the Tasks API and the ability to block calculations and make calls synchronous
To streamline the process and get rid of callbacks I set up a separate Executor for processing data and set a timeout, if the phone took too long to analyze skip a frame (3 seconds is too long time)
val barcodesTask = scanner.process(image) // Create Task
try {
// Get a synchronous result, but remember NEVER do it in the main UI thread!
// Throw a TimeoutException if the task takes longer than 3 seconds
val barcodes = Tasks.await(barcodesTask, 3, TimeUnit.SECONDS);
// Do something with results ;)
for (barcode in barcodes) {
// Get data for every found barcode
val bounds = barcode.boundingBox
val corners = barcode.cornerPoints
...
}
} catch (e: ExecutionException) {
// The Task failed close image and get another one
} catch (e: InterruptedException) {
// An interrupt occurred while waiting for the task to complete.
}
And that’s it! This way I got rid of callbacks and the error that appeared out of the blue causing the application to stop working properly — I no longer had to nest the code, and at the same time I was sure that if the phone took too long to process the information the intercepted exception would inform me that I already had to close the picture and pass the next one for analysis.