Coroutines tips and tricks: callbacks. Synchronous way to work with asynchronous code.

Alexandr Zhurkov
The Startup
Published in
5 min readJun 3, 2020

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 and onFailure .
  • 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

Initial code implementation for item consumption

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.

Wrapping ConsumeResponseListener with CancellableContinuation invoked with suspendCancellableCoroutine method

“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 until resume or resumeWithException is called within suspendCancellableCoroutine block.

Let’s get back to code! Considering all the things above here’s how we can re-write magic potion consumption

Final implementation of synchronous code execution with Billing API

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.

Initial network call executor function

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.

Helper function that suspends code execution until onReponse or onFailure method is called

Time to break down the code above to avoid any confusion:

  1. Callback is initialized within cancellableContinuation
  2. suspendCancellableCoroutine awaits for cont.resume(...) to be invoked and returns value passed to resume(...) function
  3. Initialized callback object is available for use for any code that calls callSuspendWrapper

And this is the final result of executeSync implementation:

Final implementation of synchronous network call execution

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

--

--