I promised myself once: “I will never write an article about architecture, cause it is the banalest topic”. I have never been so wrong in my life. Here is the overview of reactive architecture evolution in my projects, since the first
pod 'RxSwift' till a few years of reactive stack programming.
No silver bullet. Only experience with production examples and opinions on different cases.
Check out this repo with examples for better understanding. Folders inside Screen/Search corresponds with topics of this article.
When a developer makes his first steps in reactive programming, he or she quickly realizes that there are two clearly separated components in every screen he implemented.
First is about the poor mapping of user intents and binding them to business logic inputs. For instance, a registration screen with few fields and “Sign Up” button can be implemented like this:
It’s clear that a user has only one intent
signup. It combines all inputs from UI and emits only when the signup button is tapped.
The second component encapsulates your business logic. Probably, it’s something about mapping and filtering events representing user’s intents. We want to map user signup intent to the corresponding network request. Note, that
catchError idiom helps us to prevent completion of the sequence. A new request will be sent every time button is pressed.
These are basic examples and, of course, you saw the names of these components many times. They are widely known as
ViewModel. And together they are the most popular architecture for RxSwift or something which has UI-bindable properties. It’s called MVVM.
It was always absolutely clear for me where to put business logic and how to implement it. The only complicated question is how to create a convenient connection between these two layers. Connection, which will be comfortable for tests, refactoring and extending. And, in my opinion, this connection is the most important place in reactive MVVM.
My team in scal.io walked a long way trying to find the solution completely suitable for our needs and I am going to present you a breath overview. I hope this will save you a lot of time and reduce pain :)
Input as functions
Honestly, this was not the first implementation I used. The initial version had
var dataSource: Observable<[Item]>. But I quickly realized that RxSwift has a powerful system of traits and using them is actually a good idea. Traits help to create meaningful interfaces.
Driver here means that
dataSource is needed for UI stuff, probably table configuration, so items should be observed on the main thread.
User is able to search or select items, we formalize these intents as functions. But we quickly realize that it’s not convenient. Look at this part of code, where few operators are applied to
These lines should not be here. Their place is
ViewModel because debouncing, filtering and skipping is actually business logic. But
search is a function, it has no stream semantic. This function is imperative and we can’t use stream operators there, so there is no chance to move these lines to
Of course, we can implement operators in imperative style… But I can’t see any sense in this double work. RxSwift methods are well optimized and covered by tests. They are trustable and it’s better to prefer them over the self-invented wheel. That’s why functions should not be used in
Input as streams
User intent is a stream of events, this is a really good idea. We already have these concepts in
searchBar.rx.text, so what’s the point to rethink it? Let’s use the same approach for communication of our architecture components.
Streams as init parameters
The first thought is to pass all inputs from UI to
ViewModel via init like in RxExample project.
This might look like a good idea, however, I don’t think it really is. IMO,
init should take only arguments which are necessary for object creation. Can the
ViewModel live without a stream of login button taps? Definitely, yes. So
input tuple should not be here.
If I didn’t convince you, think a bit more about dependency injection. You can get required streams of UI elements only after view was loaded and outlets were injected. So, you can’t create your
ViewModel before loading a view.
viewDidLoad method will be called with nil
viewModel and only after it you will get all necessary inputs.
This approach doesn’t look safe for me. A view should be loaded only when
ViewController is fully initialized and ready to work.
Let’s just declare ViewModel properties which will be responsible for input streams.
And that’s it. Our
ViewController is just a simple namespace for declaring bindings of UI elements to business logic inputs. Just look at these lines:
Absolutely declarative and clean. Perfect? No. Actually, it became even worse from the contract point of view. Let’s remember this picture from Subject documentation.
Subject conforms to
Observer protocol, so it can errors out or complete. Subscriptions of that subject will repeat that behavior. So, if we emit an error to
search subject, our UI will be completely dead. Only application relaunch will recreate this subscription again.
ControlProperty type, which never fails, so it’s safe to bind it to the subject. However, it’s bad to have a contract, which allows actions which we don’t require to happen. So, it’s incorrect to use
Subject for expressing user intents.
Happily, RxSwift already handled that for us. One more abstraction around
Observer was introduced.
Relay just ignores
.completed events and raise a fatal error in Debug mode, you can read more about it here. In 5.0.0 release relays were moved to separate framework, so they can be used even without
Yes! Inputs and outputs can’t fail or complete. The contract is now absolutely correct. And I would like to finish this article exactly here, but I don’t want to lie to you.
The main issue — my example is too simple. This is the problem of all architectures sample projects, they work awesome only with the counter app. But when trying to scale such solutions to a production application, we meet problems. So does I.
It all starts with a really simple screen. You saw it many times: table view, pagination, activity indicator and pull to refresh. Something like that:
Even with this low complexity, code can be unclear. To be honest, I love this style and it’s easy to read for me. But I understand why it can be confusing for a new developer, which is unfamiliar with FRP.
The reason is that we can’t make a pure function from our
ViewModel. There is no opportunity to implement business logic without having internal states and side effects.
For instance, within this search app example, I have to store the current page to make pagination works. I can’t just take a stream of
reachedBottom events and map it to next page items, cause I don’t have page parameter initially. I have to store and mutate it.
I have to combine many inputs and internal states to create one output. I have to split operators chain and save intermediate stream state just to bind it to multiple outputs. That’s why there are
allSearchRequests needless variables. Just to correctly handle loading and save pagination state. Trust me, the more complicated logic you have, the more messy Rx code you will get.
I have a production example when table view content depends on six different streams:
- Current device geolocation
- Custom location selected by a user
- Currently scrolled page
- Search query
- Pull-to-refresh events
- Responses from two separate endpoints
And god, how hard it was to implement it in a readable way. One day I finally realized I just can’t do it with this Inputs/Outputs
ViewModel. And at that moment I decided to follow advice, which my granddad gave me when I was a child.
If you are looking for a solution of complicated problem, just go to Web. They already invented everything you need.
Yes, of course. Redux will save me. But I don’t want to implement async work in middleware. I need something which is closely integrated with Rx, built with its power in roots.
- combine all inputs into one stream containing enum of user intents
- combine all outputs into one stream of state objects
I only used ReactorKit in production, so here are its benefits over pure RxSwift architecture. Repo has detailed documentation, I will provide only a breath highlight of benefits.
Action enum formalizes all possible user intents.
State is a simple struct that incapsulates screen state. And
Mutation enum formalizes all possible changes which can be applied to the state.
My favorite thing here is that mapping user intents to business logic requests are clearly separated from mutation of the state. Each
Action should be mapped to
Observable<Mutation>. When mutations stream emits,
reduce the function is called with current state and new change. And god, it has never been so easy to change state.
Just look at this code, it became much more:
- readable, no more complicated combining Rx operators, no more messy bindings to multiple outputs
- simpler, it’s really easier to apply changes in an imperative way
- explicit, all mutations are now collected in one place (not in random
do(onNext:)at 300th line), easy to debug them
- scalable, with the increasing complexity of business logic, you will just add a few more cases here
- testable, it’s very easy to test this function, cause it’s pure.
Good knowledge of a reactive framework is not enough to build a scalable application. Reactive code should be organized somehow, and you should set it as a standard in your team. This will help every dev easily change and extend code pieces.
Today I showed you my team standard progress. This implementation of UDF is called MVI and widely used in Android world. I suggest you to deeply think about the question “Does my reactive code is really as simple as I want it to be?”. And if not, probably MVI is what you actually need.
Try to adopt it within one screen and I promise, you will want more of it. And, remember, let Rx go in your heart.
I am going to share more RxSwift experience on Saint AppsСonf. Meet me there!
If you enjoyed this tip, please give it a clap (50) and share to help others find it! Follow me to read more about Rx. Twitter: M0rtyMerr