At Trello Android, we’ve been considering switching from RxJava to coroutines.
We’ve already been using coroutines here and there, but it wasn’t something that we could consider replacing RxJava with until recently because of missing functionality. Now, with the addition of StateFlow and SharedFlow, coroutines are comparable to RxJava.
In order to evaluate a move from RxJava to coroutines, I wrote down a list of pros/cons for each framework.
Mature And Battle-Tested
RxJava works for us and many others. We are already using RxJava throughout our app successfully. We’ve written utilities on top of RxJava. Our team is experienced with RxJava. We know how to avoid its pitfalls and work around its quirks.
Kotlin coroutines are a younger, unproven technology. We don’t know for sure that it’ll work for every use case. Switching to coroutines would require some period of education (of the team) and discovery (of best patterns). We would have to rewrite some tools to support coroutines instead of RxJava (e.g. Mobius). We don't know its corner cases or quirks.
RxJava’s API is stable.
Coroutines are filled with APIs that are marked as experimental and may still change.
RxJava is famously difficult to debug, but Kotlin coroutine stack traces give RxJava’s a run for its money. We are already experienced in debugging RxJava; we’ll have to relearn how to do so with coroutines (though tools that make debugging easier are coming soon).
RxJava can be used with any Java-compatible language, whereas Kotlin coroutines can only be written in Kotlin. This is not a concern for Trello Android, as we are all-in on Kotlin, but could be a concern for others.
(Note that this just applies to writing code, not consuming it. A library could use coroutines internally but expose a normal Java API to consumers.)
RxJava is, as it says on the tin, limited to Java. Coroutines work on any Kotlin-supported platform, so if we ever want to share asynchronous code between Android and iOS we could do it with coroutines.
RxJava has many different streaming types (
Completable) in order to represent each type of asynchronous streams. Kotlin coroutines can model the same streams with only two cases: suspend fun and
This is dramatically simpler for consumers, not only because there are fewer types, but also because you can always just call suspending functions in operator lambdas (vs. combining multiple RxJava streams via complex operator chainin).
Any tool building on top of coroutines will have a simpler implementation, too - it is common in RxJava to write the same utility five times (one for each stream type), whereas that would be unheard of in coroutines.
Writing custom operators in RxJava is a gigantic PITA. To truly get it right, you have to understand the internals of RxJava (due to complicating factors like back-pressure), which is an extremely high bar achieved by only one person (...I’m only partially joking).
RxJava streams are prone to leaks, where a stream continues to process items even when you no longer care. Kotlin coroutines use structured concurrency, which makes it much easier to manage the lifecycle of all your concurrent code.
Easier Back-Pressure Handling
Back-pressure happens when the source of a stream emits items faster than the downstream can consume them. It’s a nasty problem in RxJava and the source of a lot of complication in its codebase because downstream consumers are constantly having to collaborate with upstream producers to avoid backpressure issues.
Flows handle back-pressure in a much simpler way by suspending producers when consumers become inundated with too much data. That means you don’t have to concern yourself with backpressure details nearly as often in coroutine code.
Flow has at least as good (if not better) performance than RxJava.
Reactive Scrabble is the usual benchmark tool for streaming comparison, and Kotlin has a version of the benchmark in their repository. By their metrics (lower score is better), Flow outperforms RxJava in both the standard and optimized setups.
It’s worth noting that there is very good interoperability between RxJava and coroutines.
Kotlin provides a bunch of converters between RxJava and Flow. Without them, you’d have to migrate a codebase wholesale, an unfathomable prospect. Due to the converters, you can transition incrementally to Flow. It also means you can continue to consume RxJava-based libraries even if you are otherwise using Flow.
Flow also includes a migration tool: deprecated functions that can automatically replace RxJava operators with their Flow equivalents. You just turn the base RxJava stream into a Flow and these replacements will start popping up in the IDE. (Note that this is NOT a compatibility tool - the deprecated functions are no-ops.)
The major downside for switching from RxJava to coroutines is that they are a cutting edge technology and you will get cut by it. You will have to learn to use a new technology. You will have to figure out new patterns and best practices. You will have to write new utilities. And you will definitely, someday, write a major bug (or two, or three) because you didn’t know about some corner case of how coroutines work.
These are all short-term concerns though. There are big upsides to coroutines: multiplatform support, simpler API, structured concurrency - in the long run, these advantages will pay dividends. For me, the overall argument between the two frameworks is one of maturity vs. features. Coroutines will eventually be as mature as RxJava, but RxJava is never going to be able to integrate some of coroutines' best features.
For our part, Trello Android is going to start slowly adopting coroutines as a replacement for RxJava. As with every major change, we’re taking it step-by-step in case unexpected dilemmas arise, but so far things are looking good. That said, depending on your risk tolerance, you may want to delay replacing RxJava with coroutines until it’s been slightly more battle-tested and stabilized.
Many thanks to all of my Trello coworkers for our many discussions comparing RxJava to coroutines which honed my thoughts on the matter; and Manuel Vivo for reviewing a draft of this article