Overriding RxAndroid Schedulers in RxJava 2
RxAndroid now supports RxJava 2, and with it, a new set of APIs for overriding Schedulers. In this post, I’ll explain these APIs, their motivation and show you how to use them to address a common Android unit testing problem.
But first, a bit of background
In both RxJava 1 and 2, Schedulers are used to control the concurrency and timing of operations. They are applied primarily using the observeOn
and subscribeOn
operators. Additionally, some other operators, such as delay
or debounce
are executed on specific Schedulers by default, but are also overloaded to allow you to use an alternative Scheduler.
RxAndroid defines its own Scheduler through AndroidSchedulers.mainThread()
. This Scheduler can be used to meet the Android requirement that all UI actions are performed on Android’s main thread.
Here’s a simple example, written using RxJava 2:
public final class MainPresenter {
// Other bits removed for brevity…
void bind() {
disposable.add(bookstoreModel
.getFavoriteBook()
.map(Book::getTitle)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view::setBookTitle));
}
}
The use of observeOn(AndroidSchedulers.mainThread())
ensures that the View is updated on the appropriate thread.
However, in some contexts it is useful to override the behavior of these “out of the box” Scheduler instances. This is most commonly done when testing; perhaps to avoid the pitfalls of asynchronous operations or perhaps to verify specific timing-related scenarios. In those cases, we need to devise a way of supplying these alternative Scheduler instances to our operators.
There are various ways of achieving this, each with their own pros and cons. For RxAndroid, arguably the least intrusive solution is to use the RxAndroidPlugins
hooks. They work by altering the initialization and creation phases of AndroidSchedulers
, so that it returns instances of our own choosing.
Our testing problem
So when exactly would you want to override the default AndroidSchedulers.mainThread()
Scheduler?
When it comes time to test our code, the generally preferred technique is to run the tests on the JVM, rather than using the significantly slower on-device Android instrumentation tests. For our example code above, we could write a JUnit test as follows:
@Test
public void bind_setsFavoriteBookTitle_whenGetFavoriteBookEmits() {
Book book = Book.create(“Scheduler Adventures");
when(bookstoreModel.getFavoriteBook())
.thenReturn(Observable.just(book)); presenter.bind(); verify(bookView).setBookTitle(“Scheduler Adventures");
}
However if we run this test, the execution will abort with an error message:
java.lang.ExceptionInInitializerError
at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:35)
at io.reactivex.android.schedulers.AndroidSchedulers$1.call(AndroidSchedulers.java:33)
at io.reactivex.android.plugins.RxAndroidPlugins.callRequireNonNull(RxAndroidPlugins.java:70)
at io.reactivex.android.plugins.RxAndroidPlugins.initMainThreadScheduler(RxAndroidPlugins.java:40)
at io.reactivex.android.schedulers.AndroidSchedulers.<clinit>(AndroidSchedulers.java:32)
at com.petertackage.rxjava2scheduling.MainPresenter.bind(MainPresenter.java:21)
at com.petertackage.rxjava2scheduling.MainPresenterTest.bind_setsFavoriteBookTitle_whenGetFavoriteBookEmits(MainPresenterTest.java:61)
)
<snip>
This is because the default Scheduler returned by AndroidSchedulers.mainThread()
is an instance of HandlerScheduler
which relies on Android dependencies which are not available to be instantiated on the JVM.
How can we fix this? In our test environment, we want to override the default AndroidSchedulers.mainThread()
Scheduler and return an instance which does not have these Android dependencies and can safely be instantiated. Furthermore, this Scheduler should operate synchronously.
RxAndroidPlugins to the rescue
RxAndroid’s RxAndroidPlugins
class provides three main hooks for overriding RxAndroid’s Schedulers.
setInitMainThreadSchedulerHandler(Function<Callable<Scheduler>, Scheduler> handler)
This allows you to override the default Scheduler instance. It takes a lazily evaluated Scheduler override and the result is statically applied the first time you access the AndroidSchedulers
class.
setMainThreadSchedulerHandler(Function<Scheduler, Scheduler> handler)
This allows you to provide a secondary dynamic override to the default Scheduler instance; it takes precedence over the Scheduler provided via setInitMainThreadSchedulerHandler
. You can also clear the override by calling setInitMainThreadSchedulerHandler(null)
. It is evaluated each time you call AndroidSchedulers.mainThread()
.
reset()
This does almost what you think it should; it clears all RxAndroid Scheduler overrides. Although, as I will discuss later; this is actually not sufficient to revert to RxAndroid’s “out of the box” behavior.
Note that the APIs for overriding Schedulers in RxAndroid have been designed to work in the same way as RxJava 2’s equivalent; RxJavaPlugins
. So you don’t need to figure out these things twice!
How exactly?
So with these hooks in mind, how do we get our test running successfully? We want to call one of the RxAndroidPlugins
methods, but which one?
Due to the AndroidSchedulers
initialization sequence, we need to set an override at the earliest possible stage; by calling setInitMainThreadSchedulerHandler
. It’s not enough to set a dynamic override via setMainThreadSchedulerHandler
; that would be applied too late.
We can then modify our example test by adding the JUnit hook @BeforeClass
, so that we override the default before any of the tests are executed and importantly; before AndroidSchedulers
is accessed.
@BeforeClass
public static void setupClass() {
RxAndroidPlugins.setInitMainThreadSchedulerHandler(
__ -> Schedulers.trampoline());
}
Now when we run our test, the AndroidSchedulers.mainThread()
call will return RxJava’s JVM safe Schedulers.trampoline()
instance. And most importantly; our test passes!
The Gotchas
Before we get too excited, it’s worth highlighting two important caveats for using setInitMainThreadSchedulerHandler
:
- As mentioned, you must invoke it prior to accessing the
AndroidSchedulers
class, otherwise the default instance Scheduler will be evaluated via static initialization, causing the aforementionedExceptionInInitializerError
to be thrown. - Once the default
mainThread
Scheduler is evaluated, there is no way to reset or change it. If you look into the implementation, thereset
method might clear appropriate internal fields, but the default instance is not actually being re-evaluated; it’s final. That’s why, strictly speaking, there’s no need for areset
call in our example.
These are in fact the behaviors by design, rather than oversights.
At a glance
So to summarize their intended usage:
- Use
setInitMainThreadSchedulerHandler
once to define your default (“baseline”) Scheduler and then; - Use
setMainThreadSchedulerHandler
to dynamically set or reset your Scheduler thereafter. - Use
reset
to clear dynamic (not default) overrides.
I hope this helps!
If you want to see for yourself, here’s the full source code that I’ve used in the examples.
Also see my follow up article about another technique: the SchedulerProvider
approach — https://medium.com/@peter.tackage/an-alternative-to-rxandroidplugins-and-rxjavaplugins-scheduler-injection-9831bbc3dfaf