Coroutine Misconceptions: Whose Context Is It Anyway?

I know you like RxJava, but…

Brian Yencho
Livefront
5 min readMay 7, 2021

--

TL;DR: You should never need a call like withContext(...) { suspendingFunction() }.

Photo by John Anvik.

Consider the following all-too-familiar task on Android: you must make a network request and update the UI with the resulting data. RxJava veterans will write the following without hesitation:

A pretty typical call chain when using RxJava to fetch a network value and then update the UI.

(I have assumed for simplicity that both a success and an error will be wrapped in the same object and emitted in the main channel.) Note the subscribeOn operator that places the upstream call on a background I/O thread and the observeOn operator that places the downstream updateUiWithResult call on the main thread. So common is this combination that most RxJava users writing in Kotlin are likely to have their own helper extension to apply both simultaneously:

A simplified call chain where all the thread application is handled in a single call.

The question I am here to discuss, however, is what is the best way to represent such a call chain using Kotlin coroutines? It is not difficult to come across examples like the following:

Is this right?

or

How about this one?

or even

Maybe this???

These all compile and function correctly but I’d argue they should be treated as incorrect (or, at the very least, non-idiomatic and unnecessarily redundant). These all stem from the thinking that it is the caller’s responsibility to supply background threading to background operations and I contend that this is RxJava-style thinking that should not carry over to coroutines…and I’m not the only one.

The Right Way

So what does it look like if it is not the caller’s responsibility to supply threading? Consider the following:

This gets it right.

There are two very important things to note here:

  • viewModelScope (and the equivalents in an Activity or Fragment such as lifecycleScope) uses Dispatchers.Main by default.
  • getNetworkResult has not had any manual threading applied by the caller here using something like withContext(Dispatchers.IO).

This first point means that any function calls (suspending or otherwise) that must be run on the main thread can be done so without any additional handling within this scope. Also, because you always need a CoroutineScope to call a suspending function in the first place, the question of “downstream threading” is naturally already answered before you even write the suspending calls themselves.

The latter point here is really the key, though. For this to be a safe call that does not result in a NetworkOnMainThreadException or otherwise block the main thread, we must assume that getNetworkResult now has threading “pre-applied” in a sense. This may go against the instincts of many RxJava users, but it is, in fact, the desired goal of both JetBrains (who develops Kotlin and coroutines) and Google (who continues to take a Kotlin-first and seemingly coroutines-first approach to new Android features).

What JetBrains Says

First, consider Roman Elizarov’s article on the relationship between blocking calls and coroutines. He makes very clear that the Kotlin community should adhere to a pattern in which “suspending functions do not block the caller thread”.

When writing a suspending function to wrap a blocking call, simply applying the suspend keyword does nothing to create a well-behaved suspending function and the Intellij IDE / Android Studio will even warn you about this. (I’d even go so far as to say it should be made a compilation error rather than a mere warning). It is the explicit application of withContext that imbues the function with the correct behavior:

The right (and wrong) ways to convert normal blocking code into a suspending function.

Failure to do so simply results in a function that must be called within a coroutine scope but still acts like a normal blocking call inside it. If we were being generous, this could be considered a “hint” that the call is blocking. However, because the most common coroutine scopes that an Android developer will deal with are ones that still run on the main thread (like viewModelScope), in a way this is just pushing the same problem to a different place and requiring more machinery to deal with it. With the correct Dispatcher already applied within the function body, this new function can be called safely from any coroutine scope and work correctly. This also means that you should never have to write something like withContext(...) { suspendingFunction() }.

What Google Says

This brings us to the general idea of “main-safety”. This is Google’s way of labeling suspending functions that can be called from a coroutine scope that uses Dispatchers.Main without any additional handling:

Suspend functions should be main-safe, meaning they’re safe to call from the main thread. If a class is doing long-running blocking operations in a coroutine, it’s in charge of moving the execution off the main thread using withContext. This applies to all classes in your app, regardless of the part of the architecture the class is in.

This is really just an Android-specific way of stating Roman Elizarov’s key point about properly constructing suspending calls.

Third-Party Libraries?

And what about third-party libraries that feature suspending function calls? Any common, well-maintained library used by the Android community (such as Retrofit) is going to adhere to these same principles as well. In fact, while it was always an option to have threading pre-applied to RxJava types supplied by Retrofit, this is the only way you will receive a suspending function from it. This will typically mean that the only code where you really need to worry about making suspending functions “main-safe” is your own.

This really all comes down to a question of responsibility: who’s in charge of supplying background threading to calls that require them? Is it the consumer of the method or the writer of the method? Coming from RxJava — where it was more common to leave it to the consumer — it can be easy to try and carry over that style of thinking to coroutines. (I know that this is certainly how I approached it when first learning about them myself). As a community, though, we have a chance to establish a better, stronger pattern this time around. We have a chance to stop thinking about placing a suspend function on a background thread and start demanding that they already be on whatever thread they require. We have a chance to speak with one voice and say…

This is the way.

Brian works at Livefront, where he *may* have been advocating for a suboptimal RxJava threading pattern this whole time…

--

--