Some asynchronous options on Android
Have you ever seen a “xx frames dropped” log in your logcat console? This simply means you’ve been blocking the Android’s main thread for too long (more than the 16ms needed for a 60fps refresh rate). At best, your users missed the slight lag in your animations, but at worst, they ended up with an “Application not responding” system dialog from Android. Which is bad. Not Application crash bad, but close.
Countless tweets and blog posts will give you all sorts of advice to improve your app performance and track the smallest thread blocks in your app, but for many of us, it just starts with handling asynchronous or long operations properly. Or to simplify it even more: how do you deport computation or I/O tasks on background threads and come back to the main thread to display your results?
Concurrent programming is quite challenging. I just got Brian Goetz’s “Java concurrency in practice” and intend to catch up on the topic with some good reading, but after a few years of Android development, I’ve come to see and possibly try a few different options. The list in this post is by no means exhaustive, but it sums up what I consider the most common options for asynchronous programming on Android.
Java Thread API
Your first option, if you’re an old school java programmer is to use the good ol’ thread API. Starting threads to execute your Runnable tasks, having them wait and join is quite a low level approach, and not really one I’d recommend, unless you really master Brian Goetz’s book. In my opinion, that’s the easiest way to get deadlocks and lose track of which thread is doing what. This option is pretty hard to debug, let alone test automatically. And much easier options exist today, especially on Android.
So leaving that aside, the first way to deal with background operations that comes to mind when it comes to Android is AsyncTasks. That used to be a valid option, and one that’s been used by millions of Android developers. Extending AsyncTask and adding your code in the doInBackground method is all it takes to transfer a long calculation to another thread, and the other methods allow you to handle lots of simple use cases. Tracking progress, running a subtask on the main thread before the main work begins or after it ends is just as simple.
That simplicity explains why so many people use AsyncTasks. But it also comes with a severe pitfall: a risk for memory leaks. It’s not the AsyncTask in itself that’s the risk but the fact that this class is so simple to use that you could easily throw a little anonymous class in your Activity to do the job and not realize how wrong what you just did is: if you implement an AsyncTask as an inner class or an anonymous one, your task will keep an implicit reference to the enclosing class, that is your activity. This means that your activity can’t really be destroyed and garbage collected in case of a device rotation (or any other configuration change) until that background task is finished.
Anyway, if your use cases are quite simple and you’re careful not to keep any reference to a context or view object (either implicitly, explicitly, directly, indirectly…), that could be all you need to delegate blocking tasks to a background thread. You can even use them with executors and pools of threads to keep things a bit cleaner.
The Android SDK offers a few different possibilities when it comes to services. Foreground services, background services, bound services, Intent services… There’s some new vocabulary for you to check out if you’ve never heard of these words. All those terms can also be a source of confusion, which is why I won’t dive into that topic here. There would be plenty of material here for another post.
All I would note here is that services are generally associated with long running tasks in the background. Tasks that do not necessarily depend on the app’s UI to be displayed. Think about a synchronization job running from time to time to update info on the device or a music player running even when the player’s window is closed.
Moving to the next option: some libraries already offer the option of synchronous (aka blocking) or asynchronous (aka non-blocking) calls. It’s very thoughtful of their developers to offer the two options, but they both come with an implicit cost:
- If you choose to use the blocking calls, you’ll have to handle the threading issues yourself. Which means you’re back to using the other points in this list.
- If you choose to use the non-blocking calls, you won’t have to think about those background threads, but you’ll have to use some type of callbacks to be notified when the calls end.
Taking the latter path can be just as easy as using AsyncTask. A single callback is easy to handle, and you could probably return to Android’s main thread through a runOnUiThread call in your activity. Wrap that in a lambda if you’re a Kotlin fan and you’re done.
Just like AsyncTasks, callbacks work better for single shot tasks. If you’re dealing with a complex scenario involving different execution threads and sequences of operations, you’ll soon reach the limits of this approach.
One step further, you’ll find event buses. Event buses offer a few advantages compared to the previous choices:
- They allow to limit coupling between your components by relying on events emitter and receivers that don’t really have to know each other to work together.
- They also allow you to implement more complex execution paths by triggering follow up events after you’ve finished processing one.
The downside is that it can be a bit tricky to keep track of all the combinatorial possibilities. A certain event could trigger one or more other events that could result in some forms of chain reactions and race conditions, and assessing the exact state of your app or the exact chain of events that led to a certain bug might become quite a challenge. Writing unit tests involving event buses is also quite tricky.
In the last few years, another possibility has been gaining some momentum, not only for java but for most platforms. Historically derived from the .Net world, reactive programming has been advocated by a lot of people. Reactive Extensions (aka Rx or ReactiveX) produce libraries for most common languages and imply a different mindset to handle asynchronicity.
RxJava, which can be used on Android as on any java project, contains a collection of classes and operators that help you reverse certain control flows without losing control of your data streams and events (RxAndroid is a little extension that completes RxJava with specific Android additions: manipulation of Android’s main thread, conversion of user events to observable streams…). The whole concept rests on a combination of the classic Observer and Iterator patterns.
Observable objects used by Rx could be described as asynchronous collections. When a standard Iterable exposes hasNext() and next() methods, and uses exception to signal processing errors, an Observable offers the same data as event callbacks: pull interactions with Iterable become push interactions with Observable. An Observer will have to subscribe to an Observable to be notified of next, complete and error events.
This might look like another callback approach, but Rx is actually much more powerful than that: its main strength, in my opinion, lies on the fact that it brings a bit of functional programming. This is evident in the use of the multiple operators available in the RxJava library. Dozens if not hundreds of operators exist to allow you to transform your data, combine or synchronize your streams, handle time operations, switch thread, etc.
With this huge toolbox, chaining two calls is now as simple as using a flatmapping them, manipulating threads is done with observeOn or subscribeOn, collections can be merged with a zip and data can be mapped to more appropriate types to match your preference. Complex algorithms involving multiple asynchronous operations are now much more readable.
And last but not least, Rx programming allows you to write asynchronous code in a much more testable way. Even when you code involves time operations (delays, time outs…), you can use test schedulers to simulate time and check that your code behaves properly in milliseconds (no need to block your unit test for 30 minutes waiting for a time out to expire).
I could go on with many more reasons to embrace reactive programming, but this paragraph is already unfairly long compared to the others, so I’ll just conclude it with a little warning: depending on your background, changing the way you design asynchronous code to make the most of Rx can a be a bit daunting and takes some good practice. It’s no secret that Rx has a steep learning curve, and simple cases might not warrant such an effort if you’ve never used it in the past, but as a well rounded developer, spending some time on getting familiar with the possibilities it offers will definitely be worth it.
To conclude this post, I couldn’t pass a new option that appeared with the rise of Kotlin: Kotlin coroutines. The concept of coroutines itself is not that new. It’s been around for a good fifty years and lots of programming languages had them already. Coroutines simply allow you to write asynchronous code in a way that looks a lot like synchronous one. The trick is that coroutines are basically blocks of code that can be suspended.
With a few key functions like async, await, defer, coroutines quickly allow you to identify which parts of your code are suspendable, which are to be executed on another thread, where to send the result of an asynchronous computation, etc. As of today, coroutines are still presented as an experimental feature in Kotlin: according to JetBrains, this does not imply that they’re not safe enough to use in production but simply that the API is still susceptible to changes.
I haven’t had much experience with, so I am not entirely sure how easily you can test asynchronous code based on coroutines.
With asynctasks, event buses, reactive programming, coroutines and quite a few less common options, you have lots of different ways to handle asynchronous use cases in your code. I don’t think any of these choices is inherently good or bad, but they all come with their strength and their pitfalls. The main risk is generally that asynchronous programming is a bit tricky and libraries that claim to oversimplify it are either very limited or not as safe as they appear.
It is probably obvious here that my preference today goes to RxJava, but that doesn’t mean you should definitely use it for each and every app. The take away here is that you should research your options and compare them. Take some time to practice and get confident with them so you know the risks and how to avoid them.