The curious case of RxJava Disposables A.K.A. what happens when you dispose a Disposable

Piotr Zawadzki
Aug 15 · 7 min read

In RxJava 2, there’s this concept of Disposables. They’re useful when e.g. you make a long-running HTTP request, but as the request is still in progress you are no longer interested in the results of that request. This might look like this in code:

Making a request and disposing the Disposable in onDestroy()

I was playing around with Disposables by calling dispose() on different reactive chains with various operators used and I was actually surprised with the results. Therefore I decided to share the results in this article 🤓

All the code referenced in this article is available at the bottom of the article. You’ll also find some cheat sheets there.

I’m not going to explain how Disposables work as there’s already a great article about this written by Niklas Baudy (link below), but I’ll present a number of examples showing what would happen if we disposed a Disposable in various Rx flows.

Setup

In order to test what happens when you dispose a Disposable we are going to write a series of unit tests. Each test will consist of an Rx chain to which we are going to subscribe with a TestObserver. We are going to validate which values were emitted (if any) and if the flow completed. In these tests we’ll use different operators and check where the flow stops. Our general test setup is going to be as follows:

We will increase counter in subsequent operators and check the value at the end of each test to verify where the flow stopped.

In the example above, TestSchedulerRule is a JUnit Rule which sets a TestScheduler so that we can test asynchronous code easier with RxJava:

We are going to test different operators available for Observable, Single and Completable classes.

Disposing Observable-based flows

Example with flatMap + map + doOnNext

For starters, let’s see what happens when the disposal happens while we execute a long running operation in Observable#fromCallable which is inside Observable#flatMap. In this example, the initial Observable#flatMap is followed by Observable#map, Observable#onOnNext and Observable#flatMap.

When we run this test it will print:

fromCallable -> value: 1
disposing
doOnDispose: observable has been disposed
map -> value: 1
doOnNext -> value: 1
flatMap -> value: 1

The first value will be also received by the subscriber, but we won’t receive a completed callback event.

switchMap

In this example, the initial Observable#flatMap is followed by Observable#switchMap and Observable#map after that.

When we run this test it will print:

fromCallable -> value: 1
disposing
doOnDispose: observable has been disposed
switchMap -> value: 1

Unlike the previous example, no values are received by the subscriber. Also, the flow stops after Observable#switchMap — the code inside Observable#map block doesn’t get executed.

concatMap

In this example, the initial Observable#flatMap is followed by Observable#concatMap and Observable#map after that.

When we run this test it will print:

fromCallable -> value: 1
disposing
doOnDispose: observable has been disposed

Similar to the previous example, no values are received by the subscriber. However, the flow stops before executing the code in Observable#concatMap block (the code inside Observable#map block also doesn’t get executed).

flatMap with Single#just + toObservable

In this example, the initial Observable#flatMap is followed by a second Observable#flatMap with Single#toObservable and Observable#map after that.

When we run this test it will print:

fromCallable -> value: 1
disposing
doOnDispose: observable has been disposed
flatMap with toObservable() -> value: 1

No values are received by the subscriber. Just as with Observable#switchMap example before, the flow stops after the second Observable#flatMap — the code inside Observable#map block doesn’t get executed.

flatMapSingle

In this example, the initial Observable#flatMap is followed by Observable#flatMapSingle and Observable#map after that.

When we run this test it will print:

fromCallable -> value: 1
disposing
doOnDispose: observable has been disposed
flatMapSingle -> value: 1

No values are received by the subscriber. The flow stops after Observable#flatMapSingle — the code inside Observable#map block doesn’t get executed.

Observable.create

In this example, the initial Observable#flatMap is followed by a second Observable#flatMap with Observable#create and Observable#map after that.

When we run this test it will print:

fromCallable -> value: 1
disposing
doOnDispose: observable has been disposed
flatMap -> value: 1

No values are received by the subscriber. Just as with Observable#switchMap example before, the flow stops in the second Observable#flatMap — the code inside Observable#create block doesn’t get executed just as the code in subsequent Observable#map block.

Side note — when creating Observable with Observable#create you should also check if ObservableEmitter is not disposed and do not emit any values if it is.

Disposing Single-based flows

fromCallable

In this example, the initial Single#fromCallable is followed by Single#map.

When we run this test it will print:

fromCallable
disposing
doOnDispose: observable has been disposed

No values are received by the subscriber. Also, the flow stops right after Single#fromCallable — the code inside Single#map block doesn’t get executed.

This behavior is different from what we’ve seen with Observable where the flow was either stopped or not depending on the subsequent operators. With Single#fromCallable there's no point in testing subsequent operators as the code inside of them would never be executed.

In the next sections, we’re going to test disposing the flow while the long-running operation happens inside different operator blocks.

flatMap

In this example, we are going to dispose the flow in Single#flatMap.

When we run this test it will print:

fromCallable -> value: 1
map -> value: 1
disposing
doOnDispose: observable has been disposed
flatMap -> value: 1

No values are received by the subscriber. Also, the flow stops right after Single#flatMap — the code inside Single#doOnSuccess block doesn’t get executed.

map + subsequent doOnSuccess

In this example, we are disposing the flow in Single#map which is followed by Single#doOnSuccess.

When we run this test it will print:

fromCallable -> value: 1
map -> value: 1
disposing
doOnDispose: observable has been disposed
doOnSuccess -> value: 1

The value will be received by the subscriber and the code in the Single#doOnSuccess block gets executed.

map + subsequent flatMap

In this example, we are disposing the flow in Single#map which is followed by Single#flatMap.

When we run this test it will print:

fromCallable -> value: 1
map -> value: 1
disposing
doOnDispose: observable has been disposed
flatMap -> value: 1

No values are received by the subscriber. Also, the flow stops right after Single#flatMap — the code inside Single#doOnSuccess block doesn’t get executed.

map + subsequent flatMapCompletable

In this example, we are disposing the flow in Single#map which is followed by Single#flatMapCompletable.

When we run this test it will print:

fromCallable -> value: 1
map -> value: 1
disposing
doOnDispose: observable has been disposed
flatMapCompletable -> value: 1

No values are received by the subscriber. Also, the flow stops right after Single#flatMapCompletable — the code inside Single#doOnSuccess and Completable#toSingle blocks doesn’t get executed.

Disposing Completable-based flows

fromCallable

In this example, the initial Completable#fromCallable is followed by Completable#doOnComplete.

When we run this test it will print:

fromCallable
disposing
doOnDispose: observable has been disposed

Subscriber won’t receive a completed callback event. Also, the flow stops right after Completable#fromCallable — the code inside Completable#doOnComplete block doesn’t get executed.

This behaviour is similar to what was happening with Single. For the same reasons, in the next sections we are going to test what happens when we dispose our Rx flows when long-running operations happen inside different operator blocks.

andThen

In this example, we are going to dispose the flow in Completable#andThen.

When we run this test it will print:

fromCallable
doOnComplete1
andThen
disposing
doOnDispose: observable has been disposed

Subscriber won’t receive a completed callback event. Also, the flow stops right after Completable#andThen — the code inside Completable#doOnComplete block doesn’t get executed.

doOnComplete + doOnComplete

In this example, we are disposing the flow in Completable#doOnComplete which is followed by a second Completable#doOnComplete.

When we run this test it will print:

fromCallable
doOnComplete1
disposing
doOnDispose: observable has been disposed
doOnComplete2

Subscriber will receive a completed callback event and the code inside the second Completable#doOnComplete will be executed.

doOnComplete + andThen

In this example, we are disposing the flow in Completable#doOnComplete which is followed by Completable#andThen.

When we run this test it will print:

fromCallable
doOnComplete
disposing
doOnDispose: observable has been disposed
andThen

Subscriber won’t receive a completed callback event. Also, the flow stops right after Completable#andThen — the code inside the second Completable#doOnComplete block doesn’t get executed.

Note: up until RxJava 2.2.5 “andThen” would not be printed, since 2.2.6 it would (probably due to: https://github.com/ReactiveX/RxJava/pull/6362)

doOnComplete + toSingle

In this example, we are disposing the flow in Completable#doOnComplete which is followed by Completable#toSingle.

When we run this test it will print:

fromCallable
doOnComplete
disposing
doOnDispose: observable has been disposed
toSingle
doOnSuccess

Subscriber will receive a completed callback event and the code inside Completable#toSingle and Single#doOnSuccess will be executed.

Cheat sheets

Observable

Single

Completable

Summary

As you can see depending on whether you’re using Observable, Single or Completable, which operators you use in sequence and when the disposal happens the results can vary 😱. If there’s any lesson you can take from this, it’s not to simply assume that calling dispose() will cancel the entire flow.

All the examples from this article are available on Github 🤘.

stepstone-tech

Learn more about how Stepstone builds our systems and engineering organisations

Piotr Zawadzki

Written by

Principal Android Developer at Stepstone — passionate about technology, Android geek, photography enthusiast.

stepstone-tech

Learn more about how Stepstone builds our systems and engineering organisations

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