Coroutines tips and tricks: callbacks. Synchronous way to work with asynchronous code.
In Kotlin coroutines tips and tricks series we are going to solve common real-life issues that might appear when coroutines are used in the code.
TL;DR
Wrap callbacks for asynchronous code with suspendCancellableCoroutine; code examples are available at the end of each chapter.
After playing around with Kotlin coroutines there is a good chance that you have encountered the following dilemma:
- Writing
suspend
function. - Calling 3rd party asynchronous code that requires callback object with methods such as
onSuccess
andonFailure
. - Needing results from callback to continue coroutine execution.
You probably already see the problem here, but let’s highlight it once again — we require results from asynchronous code within a synchronous block. Let’s take a look at the real-life example in the following section.
Example: Consuming purchased items using Google Billing API
Imagine we’re selling magic potions in our app that can be bought once at a time. Each time magic potion is purchased via Google Play Billing API it needs to be consumed in order to be available for repetitive purchase.
Per Billing API documentation, the developer must call consumeAync()
and provide an implementation of ConsumeResponseListener
.
ConsumeResponseListener object handles the result of the consumption operation. You can override the
onConsumeResponse()
method of the ConsumeResponseListener interface, which the Google Play Billing Library calls when the operation is complete.
In order to notify our user that their magic potion was successfully added to the inventory, we need to check if Billing API has consumed purchased item. Let’s create an initial code draft
Nice try, but once we run it we quickly discover that isConsumed
is always false
because return
statement is called before consumeResponseListener
has a chance to be called. Unfortunately, BillingClient
doesn’t provide a synchronous way to consume item and we have to use callbacks. But there is a way to handle this — Kotlin coroutines allow to block code execution until further notice and we can use it to our advantage!
Wrapping callbacks with CancellableContinuation
Remember that coroutine functions are marked with suspend
modifier? There is a reason for that, because they are actually suspending (temporary stopping) code execution until coroutine function returns a result or throw an exception. We can apply the same logic within the coroutine function using CacellableContinuation — wrapper for the block of code that needs to be invoked before suspended function can continue. I hope that you grasped the idea behind it, so let’s take a look at the code.
“So… you are wrapping ConsumeResponseListener with CancellableContinuation that is invoked with suspendCancellableCoroutine that definned in consumeResponseSuspendWrapper function?”
Possibly confused Reader
Yeah, that’s exactly what we did. We could’ve ended this article here but it would be much better if we had an understanding of why we did it this way. In order to avoid any confusion let’s break this function down to 4 pieces:
ConsumeResponseListener
is an interface in the Billing API library that is called when the magic potion is consumed after it’s added to the user’s inventory.CancellableContinuation
is an interface representing a continuation after a suspension point that returns value of specified type T. This type of continuation might finish its execution with the invocation of resume(result: T) or resumeWithException(e: Excpection) methods.suspendCancellableCoroutine
simply suspends coroutine until its cancellable continuation finishes execution.consumeResponseSuspendWrapper
function that eases usage by providing ConsumeResponseListener implementation
Once ConsumeResponseListener
receives a response from Billing API it is basically saying: “Hey, we have received a result over here, you may no longer suspend your code and resume execution with the received result”. That’s the point where we allow our CancellableContinuation
to resume execution by calling this piece of code:
cont.resume(billingResult.responseCode)
Main idea behind calling suspendCancellableCoroutine
is to suspend code execution until resume
or resumeWithException
of passed continuation is invoked. So if cont.resume(billingResult.responseCode)
wasn’t there, then our code would hang coroutine execution indefinitely. Invocation of resume
method delegates passed value to the result of suspendCancellableCoroutine
execution and this is exactly what we are going to receive from consumeResponseSuspendWrapper
function.
Concept to remember
Code execution will be suspended untilresume
orresumeWithException
is called withinsuspendCancellableCoroutine
block.
Let’s get back to code! Considering all the things above here’s how we can re-write magic potion consumption
Simple as that. Here consumeResponseCode
calls suspendCancellableCoroutine
that in turn calls billingClient.consumeAsync(…)
and suspends code execution until cont.resume(…)
is called. Latter is called only when the billing result is available, meaning that consumeItem(…)
coroutine function is executed in a sequential manner.
Example: executing Retrofit2 network call that uses a callback
Retrofit is a type-safe HTTP client for Android and Java. In other words — an amazing library for network-related operations that does most of the dirty job under the hood, simplifying the development process.
Nevertheless, developers still need to specify how to handle the response of HTTP requests, and Retrofit utilizes callbacks for that purpose. Depending on network call success onResponse
or onFailure
method will be executed.
It’s important to note that Retrofit also supports the synchronous way of executing HTTP calls by invoking
execute()
method, but we will stick with callbacks in terms of this article.
Let’s say we need to implement a generic network call executor class that would take Call<T> as an argument and return T from network response or null in case of error.
We haven’t yet defined someCallback
because we would like the code to be executed line-by-line and we need to once again utilize suspendCancellableCoroutine
. Let’s apply our new knowledge of coroutine suspension to our advantage by creating wrapping callSuspendWrapper
function that returns the nullable object of type T.
Time to break down the code above to avoid any confusion:
Callback
is initialized within cancellableContinuationsuspendCancellableCoroutine
awaits forcont.resume(...)
to be invoked and returns value passed toresume(...)
function- Initialized callback object is available for use for any code that calls
callSuspendWrapper
And this is the final result of executeSync
implementation:
That’s it for today! We’ve taken a look at the issue that might occur in production application on two examples and successfully resolved it without any boilerplate code. Hopefully, now you have an understanding of principles behind dealing with asynchronous code in synchronous fashion using coroutines.
Alex