Briefly about RxJava Logging

TL;DR — Tired of writing code to log the behavior of your Observables, Flowables, …? Don’t want to use an entire library? Check out those helper kotlin functions!


Imagine the situation when you are writing new feature using RxJava. Everything is going smoothly, your code is looking amazing and it’s time to run the app and test the feature.

Unexpectedly, the stream that you’ve created does not emit anything! What now?!

I will share here some techniques for debugging your RxJava issues that I’ve found really useful.

Possible issues (and how you can investigate them)

doOnXXX() operators

Every reactive type (Observable, Flowable, Single, Completable and Maybe) has a set of doOnXXX() operators (for example doOnError or doOnNext) that allows you to introduce side effects into your streams.

Normally we should avoid using this operators when composing our streams because side effects make the code more complex and harder to read.

Nevertheless, they are great when you are trying to figure out why your stream does not work as expected.

One mistake people make is to use onNext for debugging — with onNext we are ignoring other important events that might reveal the problem.

If I were having issues with Observable::interval I would use the following code to pinpoint possible issues:

val subscription = Observable.interval(1, TimeUnit.SECONDS)
.doOnEach { Timber.d("Each: $it") }
.doOnSubscribe { Timber.d("Subscribe") }
.doOnDispose { Timber.d("Dispose") }
.subscribe({ }, { })
  • doOnEach — prints next, error and complete events
  • doOnSubscribe — prints whenever anyone have subscribed to the Observable
  • doOnDispose — prints whenever subscription has been cancelled

Those three operators are everything you need to know how your Observable works.

Those operators differ a bit depending on different reactive stream types, check out this table for reference:

doOnEvent with Single, Completable and Maybe do not work like doOnEach from Observable and Flowable. doOnEvent can have up to two arguments — the first one for a value that comes as success and the other is Throwable. Depending if those values are null or not, this is how you can read the events:

.doOnEvent { 
success, throwable -> Timber.d("Event $success and $throwable")
}

Or you can use this helper functions:

/* Single and Maybe */
inline fun <reified T> printEvent(success: T?, error: Throwable?) {
when {
success == null && error == null -> Timber.d("Complete")
success != null -> Timber.d("Success $success")
error != null -> Timber.d("Error $error")
else -> Unit /* Cannot happen*/
}
}
/* Completable */
inline fun printEvent(error: Throwable?) {
when {
error != null -> Timber.d("Error $error")
else -> Timber.d("Complete")
}
}

Like this:

<Single, Maybe or Completable>.timer(1, TimeUnit.SECONDS)
.doOnEvent(::printEvent)
.doOnSubscribe { Timber.d("Subscribe") }
.doOnDispose { Timber.d("Dispose") }
.subscribe({ }, { })

If this is still too much code for you, with a small helper in Kotlin, you can make it a one-liner:

<Any reactive type>.timer(1, TimeUnit.SECONDS)
.log()
.subscribe({ }, { })

That will have the output similar to this one:

your.package D/HomeViewModel::methodName:20: Subscribe
your.package D/HomeViewModel::methodName:20: Success 0

Those debugging methods really helped me with my Reactive code, I hope they will help you too!

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.