Or how to free yourself from Callback Spaghetti Hell.
To illustrate Coroutines' helpfulness, I'll present some code to solve one real problem I had, although the current production code for it doesn't use Coroutines (yeah, It does feel like it is about time for some refactoring).
Disclaimer: this won’t be a deep technical explanation about how amazing this experimental feature is. I’ll keep my focus on some practical aspects of it.
The Problem: Splash Screen
Our App, BUX, started with a very simple Splash Screen, as usually is the case.
Our requirements were:
- The Splash lasts at least 0.5s
- Initialize the User session, which consists of: if there is a User logged in, fetch his information from the Backend again; if not, register an anonymous token to be later used to create a new User or Login.
- If the initialization takes more than the minimum duration, show a ProgressBar to give the user the feedback that the App is working on something.
The code to implement that wasn't beautiful, but not necessarily bad (the snippet below is simplified for the sake of clarity):
The Complication: Life isn't simple
That's pretty close to what the code initially was (although the App's original code was Java).
But it can't always be that easy, can it? As time passed, many other initializations needed to be done during Splash, such as:
- If there is a User logged in, fetch Feature Customizations (some sort of feature switches) from our Backend.
- If not, fetch a variant for the Signup flow that should be later used to create a new User or Login.
- Regardless of the User's situation, fetch some Customer Support Information (email, phone number, office opening hours, social media handles, etc) used during the App session.
To keep things short, I'll add just the Customer Support Information to the sample code, but I believe you can see how things would (not) scale:
Now we are living in a Callback Hell.
Note that for each operation that I start, I might have to add a new completion Boolean flag and we'll have to handle its error scenario separately.
Each error handling (User Session and Support Info) needs to be done separately, because if I had a generic error handling function that would call initializeApp() in case of a retry, we could run in a scenario where, e.g., if initializing the User Session fails and we trigger a full retry, we'd end up having the Customer Support information being fetched twice, concurrently. Not cool:
Now try to visualize that code with the other two operations I omitted. Yeah, I gave up too.
That code isn't straightforward. It certainly doesn't make me feel safe.
In a situation much akin to this one (it wasn't the Splash, but another part of the App where I had to perform 3 or 4 network requests to be able to show the Trade Screen) I freaked out. I told my colleagues that I was giving up and adding RxJava to the project, wasn't that one kind of the problems Reactive is very good for solving?
The Solution: Kotlin Coroutines
I was already using Kotlin for the BUX App for ~10 months and one of my colleagues gave me the insight:
What about those fancy Kotlin Coroutines? Can't you avoid Rx and use them instead? — Shurakov, Evgeny.
In this moment of disappointment, when I was already on the ropes, thinking about how much time and effort I'd have to invest to start writing proper Reactive code (I could pretty much read Reactive, at least the not so monstrous chains, but thinking in Reactive terms is an entirely different skill), this brought me back up to my feet.
Granted, Kotlin Coroutines were (and still are) experimental, but it looked SO… DAMN… GOOD.
Really, just look at the docs to see how elegant it is. Or not, I'll show you:
Here, the networking is still done via Retrofit, but we use a Call Adapter to make the network calls return the coroutine friendly Deferred, a sort of Future for Coroutines. I had created my own adapter library, Retrofit Coroutines, but I got Whartoned to it and not so much longer Jake Wharton released his own library that does the same job, in a bit better way: Retrofit 2 Kotlin Coroutines Adapter.
To my eyes, the biggest advantage of using Coroutines is readability. Look at how straightforward the asynchronous part of that code (or the whole of it) is. We were able to describe async behavior in a very linear fashion, and that is so much easier to read than the Callback Spaghetti we had before, or even a Reactive chain to solve the same issue.
Please also note how now we have a single error handling block (the catch part of it) for all the operations we might need to perform asynchronously. It is so much neater.
One could argue that having operation-specific error handling is a better approach because the App would only re-do the operation that failed, in contrast to our solution which, on any error, drops the ongoing operations and retries all of them if the user chooses to retry. But I believe that it is a case of unnecessary optimization, since you need all operations to succeed in order to proceed and the most common cause for one operation to fail, networking problems, would surely affect all of them.
Of course our UserSession and SupportInfoHelper had to go under some changes to become Coroutines friendly, but the changes were minor and, IMO, produced better code.
Coroutines are superb for asynchronous programming. That is all I'm stating. And my (now defunct) adapter library shows a few cases where it produces nice code.
No, I'm not saying that Coroutines are overall better than Reactive. That'd be silly. While both might be used to solve async problems, they are very different tools, each additionally solving different classes of problems altogether.
I'm also not saying that Coroutines are simple. While Coroutines code can charm you by their simplicity, their underlying implementation is not even close to that. It isn't that hard to use them to produce good and straightforward code, but if you want to understand how it all works under the hood, you'd better free up some time. I daren't say I understand it all.
Yes, it is still experimental, but the BUX App has been shipping with Coroutines powered async code since April 2017 and I haven't ran into problems caused by its implementation (can't say the same for my usage of it, though).
If I managed to interest you, check more about Coroutines:
- An introduction: https://kotlinlang.org/docs/reference/coroutines.html
- The repo: https://github.com/Kotlin/kotlinx.coroutines
- A more in-depth guide: https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md