Leveling out callbacks with coroutines — SMS Retriever case

Lukasz Wojtach
4 min readMar 3, 2021

--

A bulldozer leveling ground
By Protech. CC BY-SA

The story is about getting rid of callbacks with help of kotlin coroutines. How to do this and why.

SMS Retriever

Since the introduction of SMS Retriever API by Google, it is pretty easy to automatically read an SMS with an OTP code. It spares the user from the mundane task of retyping or copying such code into some smallish EditText. One can find instructions, how does this work as a whole on official google site: https://developers.google.com/identity/sms-retriever/overview

Whole process on android device may be summarized in such steps:

  • Define a BroadcastReceiver, which is going to receive a broadcast from SMS Retriever with whole SMS message in Intent extras
  • Start SMS Retriever client, check whether it was successful
  • If successful, register your BroadcastReceiver
  • Use your custom parsing to extract code from SMS message => that’s your part, we will not talk about it :)
  • Unregister receiver, if user leaves screen or otherwise cancels login action while still waiting for SMS.

If we have to do this on multiple screens it gets tedious fast. It is a good idea to extract all this logic to separate class. Assuming that we want to react on SMS automatically only when our „Enter Verification Code” screen is visible, we could code it like this:

This class API however is no friendly thing. Three methods `init(), startListening(), stopListening()` plus a callback in a constructor. If we use it in a fragment or activity it is going to be all over the place:

It would be much better, if our class could implement a LifecycleObserver interface and react on lifecycle changes automatically. This would leave much smaller trace on use-site

Still, we have our logic separated between „entry point” `smsRetriever.init()` and callback defined during class creation. There is no way to have any smsRetriever method just return an SMS message content. It must be passed in a callback.

Or maybe not

Coroutines

This is the time for coroutines to make their introduction. The hallmark of this async programming style is writing asynchronous code (which is full of callbacks, either in-your-face or under the hood) as if it were a normal, sequential, imperative code. TL, DR: coroutines can convert any one-shot callback into a suspending function:

Before:

After:

How is this possible? Coroutines library has two ready-to-use functions that allow to convert a callback into a neat suspending function:

They cause the running coroutine to, well, suspend its execution until it gets resumed manually from within a lambda. Now, the magic happens: the result of suspend(Cancellable)Coroutine function is the very same value, which we have resumed a coroutine with!

Coroutines + SMS Retriever

So, let us use this magic coroutine superpowers to retrieve SMS code without any callbacks involved. One liner FTW!

First, let us write a function, which will try to start an SMS Retriever client and return true if successful, false otherwise:

Starting an SMS Retriever is fast, there is no need to make our operation cancellable.

Next, a function, which will install a BroadcastReceiver and return the Intent received in broadcast

This operation (waiting for SMS to come) may take a lot of time and it is quite likely, that user will navigate away during the process. Thus we need to make it cancellable — unregister our BroadcastReceiver if coroutine gets cancelled:

That way we have killed two callbacks right at their source — after SMS Retriever Client starts and after our BroadcastReceiver receives broadcast with SMS content. What is the gain? Well, the worst thing with callbacks is that they do not compose nicely. We need to connect callback functions via class properties (shared mutable state, yes!) or nest them one into another. With “normal functions” - which return a result, when they are fully done — it is much easier:

See? No callbacks! No nesting!

All those functions:

may be declared as class members, but they do not really have to. There is no state shared between them. We could even make them an extensions on Context*.

*On the other hand having our CodeRetriever as interface with different implementations for google services enabled and huawei-only services enabled phones may come in handy.

What is important - we get a single public function, which returns content of Sms message (or null if unsuccessful). No fuss with callbacks or lifecycle signalling.

lifecycleScope.launchWhenStarted { … } makes coroutine automatically suspended, when lifecycleOwner is not at least STARTED. It effortlessly achieves goal of not authenticating the user, when our screen is not visible. As a side effect, it makes it possible to receive an SMS while STOPPED and hold onto received Intent until lifecycle owner gets either to STARTED or DESTROYED.

In the latter case, we may run into a situation, where we try to unregister BroadcastReceiver twice — after receiving broadcast (in STOPPED State, so we cannot resume coroutine) and when coroutine gets cancelled (Lifecycle gets to DESTROYED). A simple try-catch can protect us:

Summary

Coroutines are known as a library, which facilitates multithreading — especially making network calls or db queries on android. However a lot of Android APIs, which have nothing in common with networking and MUST be executed in Main Thread, are in fact asynchronous. Aforementioned Broadcast Receiver is a good example. Broadcasts are asynchronous in respect to our UI code, even though they are signalled on Main thread. Same is true for various animation or layout callbacks.

In fact a callback almost always implies asynchronous execution — otherwise why not use a normal function? Coroutines (and RxJava), being a tool for asynchronous programming can neatly wrap those callbacks and transform them into something more composable — like suspending function.

--

--