During my years with Rx I found it to be at the same time amazing and frustrating. Most of great RxJava presentations and blog posts try to convince you to give it a go. You really should do that, as Functional Reactive Programming will give you a new outlook on you problems. The amazement soon turns into frustration as you will be facing unfamiliar problems and you will need to find new ways to tackle them. I would like to share the most common Rx challenges that a person with less than one year of experience usually has. If your issue is not one of them, Ill introduce some approaches how to reason about you Rx streams and figure the solution by yourself
Today I would like to talk about frustrations.
We developers have those on daily basis,
The API doesnt work properly, somebodys horrible code ended up in the repo, or just the application has some mysterious issues.
We have then the need to talk about those frustration. Sometimes we talk to other developers, sometimes we talk to rubber animals.
But this practice is not meaningless, either we figure out a solution to our problem or we prevent others from repeating ours mistakes.
So what does Rx have to do with it.
In recent year there we loads or articles and talks saying how great Rx is, but is not always rainbows and unicorns, it has some bad sides as well.
Today I wont focus on the Unicorns, but rather I would like to talk about some frustrating things about Rx.
Still the frustrations are not always the same.
As you discover Rx, things that annoy you, change.
What Ive experienced, there are three stages of using Rx
But before I continue with the stages, could I ask you some questions?
- people who read anything about Rx
- people who wrote any code in Rx
- who have written any production code in Rx
- who have 1+ years of experience in Rx
I remember the first day I've been introduced to Rx. A colleague mine, Olli, was playing with something called Reactive Extensions on his free time.
He wanted to show us an example of auto-suggestion field.
Autosuggestion field is not the most interesting thing in a world, but most of the Olli know what he was talking about, so we joined the presentation.
Ive expected the talk to be about an hour, and at the end we would have an overview how we should write autosuggestion field.
The talk turned into a 15-minute presentation with a working code at the end.
Ive started to use Rx in my projects. It was amazing.
As our project grew bigger - it started to behave in a more unpredictable way.
Started to crash randomly, backpressure exceptions were occurring, it wasn't performing so well even though I thought I've put everything on background threads, or so we thought.
The biggest issue was that I was not able to reason about the code. By reasoning about code, I mean just by looking at a block of code you exactly know how it will behave.
Sometimes I even missed the good old, not reactive days.
Developers know that when using library/framework/tool you not only need to know it's API, or interface. You need to know how it works under the hood.
Thats what we did.
So weve learned how Rx works.
Next weve removed side-effects from observables, kept them clean, weve remembered to schedule tasks properly on background threads. Weve made the application maintainable again.
Not only that, adding new features started to be easier and faster. Code became more and more testable.
All of this mainly because because we were able to write code that we could reason about.
Still, there are moments when the code does not work like intended but most of the time they are either easy ways to solve or investigate it.
So lets go frustrations that weve encountered.
We all know and love anonymous classes in java.
This sentence is only half true.
If you are writing normal java code, this is not such a big frustration.
But in Rx you will need loads of those. And if you choose to still use them, it will be frustrating.
Instead use lambdas.
Doesnt matter if you will be using retrolambda, jack or kotlin.
Just pick one and enjoy!
Sometimes when I write a really complex Observable, such as this one.
When I am finished, I run the app or test to verify that it works but nothing happens.
Added logging in multiple places, changing schedulers still noting.
Then a single thought comes where did I subscribed?
Always subscribe!
Why
Again we have a complicated Observable.
We combine here two last values from first and second, and then print the result.
But again it does not print.
First we check that we subscribed, from now on in every example lets assume we subscribed.
So when I used to have this problem with combine latest I didnt know just by reading the code, how will the observable behave.
I had to fallow creation path of those obs and check if they ever emit a value.
I was not able to get it from this code.
Similar thing goes with concat which subsribes to first stream, emits its values and until complete event comes
Then subscribes to the next one.
But for some reason the events from the second one are never emitted.
Again, we dont know the reason just by looking at this code.
Being forced to go up the stream of observables everythime you want to understand the issue, is really frustrating.
There are two ways to handle this issue with types or with naming.
We went with nameing
Let me tell you the approach we use in our project.
getValue() - this is a normal way how you would expose an Observable. Same problem as before - we do not know how it will behave
getValueOnce() - First of ours naming convention - this Observable will emit value as quickly as possible after subscription
getValueStream() - this Observable can emit infinite amount of events. It does not give us guaranty that it will emit anything after subscription. The only thing it can guarantee us is that it will never complete - example mouse click events
getValueOnceAndStream - will emit event as soon as possible after subscription and will continue on emitting events indefinitely. Example, checking if device is online/has internet access and then listening if the network status has changed.
So let see how does it work with combineLatest.
Now we see that the first observable will trigger only once and then complete.
The second one will trigger and then continue with emitting events if any.
Only by looking at it, we know that the combineLatest will emit at least one value.
If the second Observable emits new items - it will combine them with the only value of the first Observable.
With this approach, it is much easier to reason about your observables. You don't need to follow the creation path of the observable to know it will behave.
Now I would like to talk about one of the most confusing and at the same time the most crucial operators in RxJava.
There are a couple of operators that have this exact signature.
As a parameter, they accept function with one argument T and result of Observable<R>, which is at the same time this is the same thing the entire thing returns.
Can anyone make a wild guess?
This signature is shared between 3 operators flatMap, switchMap, and concatMap
Story about not knowing flatMap
Explain announcer example
I cannot stress enough how important is to know the difference. If you are doing a Rx app and this difference is not clear yet, spend some time on playing around with them.
It will save you plenty of problems!
Next thing, how you should subscribe.
You could subscribe in this way, when you do not care about errors or if the observable completes.
But you shouldn't do it like that.
The reason is that if for some reason the getCountStream emits an error, not implemented error will be thrown, and that might crash your application.
Instead always implement the handler for the error case, even if you would just log the error.
One of the selling points or Rx is that you can schedule your work with ease on any thread youd like.
Still, you need to think about it most of the time when you write the code.
One common issue that I see is how people start Observables with heavy, blocking functions.
Just is a really handy operator that starts Observable stream with a value.
But people forget that it still accepts a value.
If you call the method like that, it will be executed on the same thread on which entire Observable was created, and NOT subscribed.
In those cases, you need to have a more lazy approach.
FromCallable is a really useful operator that will call the passed function on subscription so it will adhere to the subscribeOn operator.
When you create an Observable that has a costly subscription, for example, Retrofit observables, you need to watch out how often you subscribe to it.
In this example, for every subscribe, we would run the expensive function one more time.
It's ok to do it if this is exactly what you want.
But if you want to reuse the outcome of the expensive function, you either can cache it or share it.
One super power Rx has is the ease of switching threads
The key here is subscribeOn and observeOn.
Similarly to what I said about maps, you have to know the difference between them.
// Explain
SubscribeOn does not work with Subjects - if you didn't know that, your team is not the first one wondering why there is so much stuff happening on UI thread.
Retrofit explain and say about heavy lifting
Zip is an interesting operator.
It is a good way to show to new people the power of combining observables.
But in real life tasks, it could create more issues than it's worth.
First of all, it can make your code hard to read and maintain. Most of the cases when people say that Rx code is hard to manage, they mention zip.
Additionally, backpressure problems are quite frequent when using zip.
I am not saying you should not use zip, just know how it works, and what are it's strengths and weaknesses.
And in most of the cases, zip can be replaces by one of the map operators.
Debounce operator was the first one I've seen.
It was used for the autosuggestion example that I've told you about.
But it is also one of the most misused operators.
// Explain
Whenever someone has an Observable that for some reason triggers too often, that person could use it to filter too much noise.
But debounce introduces time-based side-effects that are hard to investigate.
Also, it could unnecessarily slow down your application due to the debounce time window.
It is always better to investigate why that other Observable triggers too often and fix the underlying cause.
You should always subscribe if you want Observable to trigger, I hope that's clear now.
But every time you subscribe, you create a subscription.
If you don't unsubscribe that subscription, you might create a memory leak.
There are cases when the subscription will be managed by itself, but it is always safer to do it by yourself.
But your problem might not be one of those.
The gut feeling will not always lead you to a solution of your problem.
In those cases, you need to start debugging.
Most common way to debug Observables is to use doOnNext. It will nicely tell you when events are happening.
But in some cases it is not enough, the reason why the observable is not triggering might be that it emitted an error or it completed.
I would strongly suggest to use doOnEach instead. That way you will be able to see all the events that might happen on the Observable.
And if that is still not enough, use doOnSubscribe and doOnUnsubscribe to check it the problem was with subscribing or unsubscribing.
Once someone asked me if I could sum up the benefits of Rx in 10 words? I've come up with an ok solution.
But that question got stuck in my head. After a while came up with shorter and better answer.
If you want your application to be really reactive, you can write the code without using Rx.
But the code complexity easily can get to a stage that it will be unmaintainable.
Rx is made to control the complexity and keep it in place.
After 3 years with Rx, I am cautiously optimistic about writing reactive code. I know that it is a really nice tool, but still only a tool.
Sometimes it's really useful, sometimes is only brings confusion.
Knowing when to use it and when it will only make your work more difficult is the most important skill you can acquire in Rx.