Overriding RxAndroid Schedulers in RxJava 2

Peter Tackage

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:

  1. 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 aforementioned ExceptionInInitializerError to be thrown.
  2. Once the default mainThread Scheduler is evaluated, there is no way to reset or change it. If you look into the implementation, the reset 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 a reset call in our example.

These are in fact the behaviors by design, rather than oversights.

At a glance

So to summarize their intended usage:

  1. Use setInitMainThreadSchedulerHandler once to define your default (“baseline”) Scheduler and then;
  2. Use setMainThreadSchedulerHandler to dynamically set or reset your Scheduler thereafter.
  3. 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

Peter Tackage

Written by

Developer

Welcome to a place where words matter. On Medium, smart voices and original ideas take center stage - with no ads in sight. Watch
Follow all the topics you care about, and we’ll deliver the best stories for you to your homepage and inbox. Explore
Get unlimited access to the best stories on Medium — and support writers while you’re at it. Just $5/month. Upgrade