Somebody asked me 2 questions the other day.
- Why should I use RxSwift?
- Is it good for all projects, or is it better not to use it sometimes?
I had been working with RxSwift for a couple of months so I had an idea about its benefits. I was able to answer those questions vaguely. I failed to be concise though.
After some thinking and research, I’ve found that the short answer to the first question is: Asynchrony.
This article is not an introduction to Reactive Programming. It’s just an attempt to answer those 2 questions. I will talk about asynchrony and other key positive elements of RxSwift:
- Asynchrony is simplified with Declarative Code.
- Multithreading is simplified.
- Cleaner Code & Architectures.
- Composable Components.
The list is not comprehensive. I wrote what in my opinion is the most important. I will try to give a short explanation for each point and finally I’ll answer the second question too.
1. Asynchrony is simplified with Declarative Code.
Async workflows can be easy to understand but hard to code. For instance, a sign-in use-case. It can be a complex workflow that involves several steps. Like validating user input, checking API responses, loading assets, etc. The workflow is not hard at a high level of abstraction. But to actually write it down in code and then understand it, is not that easy.
Using traditional imperative OOP style, the code spreads across objects in different layers. Nested callbacks, delegates, notifications, kvo, etc, complicate things. The code can become hard to follow. And understanding the whole picture becomes hard too.
Rx treats changes as asynchronous events in a stream. You push events into it and forget about them. That creates a channel of communication that subscribers can observe. A subscriber specifies what happens when an event occurs in a declarative manner.
Declarative code enables you to describe a complex async workflow at a high level in a few lines. You can say what happens and forget about the how. Like when the user enters its credentials. You validate them, then load the assets, then load the game progress.
Take a look at this simplified code snippet from the RxSwift Examples project. It’s an example that uses Drivers to validate username and password and sign-up to Github:
In this example you can see several things happening. The username and password are validated when the input changes. The result of that validation is published in a stream that will be observed by a view controller.
The last Driver combines the username and password inputs. Each time the user taps the login button. The username and password inputs are passed to the API that attempts to signup. When the server responds, asynchronously, the driver will publish the boolean result. A view controller can observe this and display a message, update the UI, etc.
This code is declarative. It states that when the input changes, the username and password will be validated. Also, when the login button is tapped, it will attempt to signup. All this is asynchronous. It can be easily understood, even if you don’t understand the details of RxSwift. Like what a Driver or flatMapLatest is or does. What happens here is pretty intuitive.
“Reactive Programming raises the level of abstraction of your code so you can focus on the interdependence of events that define the business logic, rather than having to constantly fiddle with a large amount of implementation details. Code in RP will likely be more concise.”
So, async situations can be simplified. Because you write less code and understand it more easily. Take a look at this article from Cocoa with Love to see a couple of examples. This becomes even more important when you need to perform work on different threads.
2. Multi-threading is simplified.
There are situations in which you need to perform tasks in different threads. Basically, to provide a smooth good UX. For example, to load things in background and show them progressively in the main thread. To achieve this, you need to code a lot of things to make your solution thread-safe. Mechanisms to perform work on different threads, like, mutex, dispatch queues, thread pools, run loops, etc.
“With reactive programming, all of the threading and lifetime management that caused a huge burden for the previous implementation are implicit — they’re still there but they’re just managed automatically.”
Using RxSwift, you can react to changes on different threads. You do this with a lot less code, less complexity, less bugs. You can listen to an event on the main thread and react in background. Finally you can go back to the main thread to show the results. The resulting code can be very simple: you observeOn different Schedulers. Like this:
3. Cleaner Code & Architectures.
I’ve mentioned a couple of times now that using RP leads to writing less code. Since you can react to events in a declarative way you can avoid certain complications. Like nested callbacks, delegates, notifications, etc. For example, in the previous login process. With a traditional imperative approach, you can end up with multiple nested callbacks. With RP and declarative code, you take that away. This results in cleaner, more readable code.
Since Rx streams are immutable, you can just react to changes and create objects that are open to extensions and closed to modifications.
In this point I decided to put together Cleaner Code & Architectures. Perhaps just because it sounds good. But anyway, here’s why I think that Rx can pave the way for Cleaner Architectures.
Because data can flow in a one way direction, Rx is directly compatible with the concept of Clean Architecture and the dependency rule. The rule states that dependency can only point inwards in a layered architecture. Dependencies should go in one direction. Rx allows you to build channels of communication between architecture layers. Layers independent of each-other, yet flexible and open to extensions.
An example to me is the use of Rx in MVVM and other MVC improvements like VIPER. You can bind objects of different architecture layers and the architecture remains clean.
Less code, loosely coupled components, separation of concerns, leading to better architectures. You can incorporate this more easily using RP.
4. Composable Components.
To me Rx is using the observer pattern in an enhanced and organized set of standards. I see it as a standardisation of the way we publish events and how we process those events. It provides structures and operations that carry and modify information changes.
The structures provide different ways of publishing information. For instance, a PublishSubject or a BehaviorSubject. They are different ways of channeling events. The operations transform, decorate, compose, etc, the information. Like map, filter, concat, etc. The key is that everyone can use the same structures and operations. No matter what they put in them.
By working with these standard structures and operations you can take your components and combine them with others in an organized manner. You can compose components, operations.
“Where Reactive Programming shines is in the creation of components and composition of workflows. In order to take full advantage of asynchronous execution, the inclusion of back-pressure is crucial to avoid over-utilization, or rather unbounded consumption of resources.”
Reactive programming is a way of working, it’s a design pattern, or a set of them for all that matters. You can create reusable composable components because of the standard structures and operations. The same standardisation comes in different languages and platforms. So If you learn Reactive Programming in Swift, you can take it with you to another platform and language.
Take a look at all the Rx languages
The learning process can be quite intimidating at the beginning. You need to be familiar with block closures and functional programming concepts.
Another drawback is that it is easy to create memory leaks if you are not careful with self references inside closures. You need to have a good understanding of memory management. Take a look at this article if you want more information on this.
Debugging could be hard too considering that stack traces are a lot bigger. Turning on the debugging info and printing the resources used by Rx helps a lot to find mem leaks.
So, going back to the beginning:
Why use it? Because you can deal with async problems in an elegant manner. Because you can write better, cleaner code and create components that can be reused in a robust codebase that can evolve.
Should you always use it? As any tool, you should use it when if it fits the problem. Consider your team knowledge, context, timeframes and try to make an educated decision. In the long term, I think it’s worth the effort.
I like the way I can architecture things and connect layers using RxSwift. I also love creating composable elements with it and the elegancy and cleanliness of the declarative code to deal with async situations.
Do you think I’ve missed something? Please let me know!