The case against Rx for going async on Android

Muhammad
8 min readNov 23, 2021

--

I guess all of us software engineers were tailors at one point , obsessed with threads.

Consider coroutines to be the new, modern , intuitive asyncTasks of the present. The reason I make this comparison is that if you go on through the official docs of android and see how do they now recommend going off the main (async) , they will suggest coroutines , just like they recommended AsyncTask in the difficult past.

However this wasn’t always the case and pre-Kotlin android needed rx; in the absolute sense. Rx created a strong case back then. Now however its not quite useful to spend precious time and effort to learn a separate technology , with its abundant operators , hefty vocabulary (Observables, Flowables, Maybes , Singles each with their own Observer types and emitting rules), operator chaining and its rules for error handling (which aren’t that apparent or “natural” to someone doing rx for the first time), when a more concise way of going off the main has been offered by Kotlins’ Coroutines and advocated by the Google. Also pretty soon it’ll be mandatory to do async via coroutines. Jetpack compose CANNOT be understood or worked with without some knowledge and experience with Coroutines.

With Coroutines , you don’t need to learn any new technology, syntax , operator chaining away from standard Kotlin. And with official Android support, you need to know even less about coroutines themselves.

If you know Kotlin, delving into coroutines would be a less than half an hour job.

Oh , I would also like to point out that RxJ still has its benefits . Like if you know Rx, you can go off the main in many different technologies with your Rx skills. We have Rx wrappers written for almost all mainstream technologies like Javascript and C#. So if the reader works in multiple technologies than I believe it is okay if you still want to keep Rx in your bloodstream.

Five reasons why coroutines is the way to go

  1. No need to learn any new programming constructs (doOnSubscribe, doOnError,subscribeOn)
  2. Use the standard library functions to perform async task via launch blocks
  3. Conventional style programming to perform complex async work
  4. Android’s official stance on how to do async work on the platform now involves the explicit use of coroutines, hence it’s time we invest into it .
  5. The ONLY WAY to perform lifecycle aware async operations. Coroutines are lifecycle aware hence they let us write code that is lifecycle aware (because of this we can fire and forget). So we can issue a long running task in a fragment , and let Android terminate the long running task on its own if the fragment has OD’d.

So what are coroutines

It’s a simple way to offload work. Below is the anatomy of a basic coroutine

A lot is being done , with very little. Largely this is how a coroutine would always look.

Coroutines operate on a concept of “structured concurrency”, which on its own is a separate article. In short it would mean that we would be performing async operations within a “structure” , such as we see one above. As we proceed everything would become more apparent .

Ingredients.

Coroutines can be mastered just by understanding the following four concepts. We would not be going into each of them in detail

  1. CoroutineScope
  2. Job
  3. CoroutineBuilders
  4. Suspend modifier

Our first coroutine

A barebones coroutine with comments on the main organs that comprise it.

Everything in coroutines surrounds around the use of suspend functions

What is a suspend function?

Well suspend functions are normal functions , that have an ability to “suspend” the coroutine (think thread) they are executing on for their entire execution span .

Consider the following picture

The fact that we have suspended a coroutine , appears as an arrow across a curvy line in the gutter

Here when the first call getStudentsFromApi() is executed , the state of entire coroutine (when we say the state of the entire coroutine , we tend to mean whatever is inside the launch block) is in a “suspended” state. This implies that until getStudentsFromApi() returns , the coroutine would NOT proceed with getTeachersFromApi().

This sequential behavior of async tasks has huge implications and allows us to write async code without caring about callbacks , subscriptions, observables, futures (more on this in a bit) , etcetera .

This also would mean that a normal try catch could be used for error catching,

Conventional try-catch for async code was unicornish before coroutines

What if you DON’T want the sequential behavior (Parallel Execution)?

In the above example , it doesn’t make sense to sequentially call our students and teachers api since they do not depend on each other.

This is where the async coroutine builder comes into play.

Lets call the teachers and the students api parallelly

The async coroutine builder is kind of a Future

Whenever we decorate a suspending function with async , the return type of that block is of type “Deferred” , on which we can call await.

Fire and forget

With the basics of the tale done , we can finally move onto some practical benefits of using coroutines in Android i.e. fire and forget. You must have noticed that in the very first coroutine example in this article we had an example of a scope which we were creating manually and then destroying it onDestroy of the Fragment we were in .

Well the good news is that , all the important scopes tuned to the lifecycle of the component we are in; are given to us ready to use and we can just call our code in those blocks and forget about it.

All the lifecycle aware scopes are present here

Flows

Coroutines are good for one shot operations. Like we just saw above in the getStudentsFromApi method. The return type of that method illustrated the fact that after the suspend function returns the List of students , that’ll be it. Its done and there is now way for it to be re-invoked.

This is a problem if we are to implement a producer-consumer type , in which we cannot afford the consumer to be one-shot. Whenever a new value is available we would like the suspending function to re-invoke.

What we want is something like this

Now Kotlin flows are absolutely comparable to the entire ethos of RxJava. Coroutines as a concept and implementation differs in great lengths with RxJava. However flows operate under the same Producer-Consumer principle that RxJava operates on. Under the hood however Flows leverage coroutines to provide a non blocking consumption experience (if a producer is taking 3 seconds to produce , we cannot block the main thread for 3 seconds , hence the consumption will also occur inside a coroutine)

Lets depict a typical producer consumer in RxJava2 , in which 10 numbers are been produced and consumed by an Observer

Producer produces a long in 8 seconds , and is consumed on the main thread

With Flows we can declare the flow in a dsl type fashion. A flow can produce from anywhere in the system , the consumption however has to happen inside a coroutine.

The producer
The consumer

This implies that we can produce from anywhere in the code and collect from the flow in a lifecycle compatible way, such that if the fragment where the collection is taking place is destroyed , the collection automatically halts.

This consequently relives the need for LiveData anymore , and all LiveData can be easily replaced with Flows. In fact this has been the official stance of the Android Team itself. In their newest code samples , they tend to use Flow instead of LiveData.

Keep in mind that Flow is a Kotlin construct and has nothing to do with the Android SDK. So there is a few additional tricks we need to perform in order to make Flow lifecycle aware. The benefit then is that with Flows we can use the entire range of higher order functions on our producer , which with LiveData we cannot.

For example, we can perform a whole range of operations on it before consuming it. Following is just a snippet.

If we had LiveData<Integer> , we could only perform a few operations on it

Where to go from here?

The following resources can be traversed through to get a better understanding of coroutines and get a feel for them .

Coroutines Official Doc

Introduction to Coroutines by one of its founders (Youtube)

LiveData and Coroutines Flow (YouTube)

Coroutines Cancellations (Co-Operative Cancellations)

Channels- A way for Coroutines to speak to each other

Cold Flows, Hot Channels

Android and Coroutines (YouTube)

Is it worth it ?

This stackoverflow post gives real life stories about developers who switched to coroutines / flow from rx and each developer gives their own reasoning for the switch.

Also this titled “article reflects upon the decreased popularity of RxJ for Android and why it would just soon be a legacy dependency in android codebases all across the development stratosphere.

My personal advice would be that it is worth the switch .

An average Kotlin developer can quickly pick on coroutines because it involves no new technology outside the domain of Kotlin. Unlike Rx which has a separate vocab and different verbs and different concepts and strategies which is quite a lot.

This recommendation is not coming from the author of this article , rather it is coming from the Android platform engineers themselves. Also very importantly we should note that, while at this moment it is highly recommended but optional to move into Coroutines, if a developer wishes to promote his application to use JetPack Compose there is absolutely no way to circumvent Coroutines as they are baked into the very fabric of the Compose framework

--

--