Reactive Architecture — Deep Dive
Welcome back. This is a continuation of my previous article on reactive architecture; where this time we will talk specifically about the building blocks of code. In other words, this post is a deep dive…an in the weeds, on the front lines, kind of post. I strongly urge you read the last post, read all blog links, and watch all videos. Then, and only then, will this post have a strong impact on you.
Remember: Reactive architecture is simple but not easy.
Four main components
I like to think of reactive architecture details as four major components and I’ll explain them in four analogies.
- Event Meat Grinder
- Main Flow Pipe
- Interacting With The Asynchronous Sea
- Old Man Listener
Event Meat Grinder
Starting from the top left of the diagram, the event meat grinder takes all those UI interactions from the user and grinds them into a single event output. A great library for subscribing to UI interactions is RxBindings. This library from Jake Wharton wraps standard android listeners with Rx bindings. This way we can subscribe to a button click, scroll, long press, etc. From that subscription we then have all the UI data needed to create a well formatted UiEvent and send it to the main pipe.
This is the most important aspect and probably the most complicated. The main pipe sits as a stream off a publish relay (Rx subject enhanced with no shortcomings) where we transform UiEvents into Actions, publish to the Interactor, and then scan results for UiModel updates.
PublishRelay allows multiple UI subscriptions a place to publish their events. It also allows long running tasks time to operate if the subscription at the end of the stream unsubscribes. Although cached observables with
merge()could be used, I believe a relay makes it easier to visualize and understand the reactive architecture flow. Since the original MVVM + Rx problem has too many subjects and streams, lowering the usage to one per ViewModel seems like a wonderful compromise and improvement. Therefore any UiEvents will publish to the subject, which in turn the subscription off the subject triggers the rest of the flow. In order to allow the subject to do work in the background while subscriptions are missing, we use a
autoconnect. The replay allows us to hold the last UiModel and the auto connect lets the task work in the background without any subscribers.
Know your ConnectableObservable.
A transformer is used to change UiEvents into Actions. Again, making this transformation allows us to modularize our architecture and achieve clean code. The Actions and Results are internal business logic entities at the center of the clean code diagram. The UiEvents and UiModels are both on the outer most layer. This allows us to enhance our testing and focus more on inputs and outputs of the stream, rather than testing each operator individually.
Publish to the Interactor. This is the open part of the pipe that abstracts internal actions and results from external UI events and UI models. In the main pipe you simply call the Interactor. It’s up to the Interactor to figure out which functionality needs to be invoked in the sea of controllers and gateways.
Scan is awonderful operator. It synchronously allows us to update our model state, based on an initial value and any result events coming in. Since we can use a case statement to choose our result type we can modularize our code even more. Scans will have a lot of code in them, but since each model update depends on a result type, abstracting the model logic outside the scan operator, helps make the main pipe code readable.
Interacting with the asynchronous sea
There are a sea of entities the Interactor will need to engage with in order to fulfill the requirements of the action. These actions and their results should be encapsulated inside the Interactor tied to the ViewModel. Using the same transformer techniques as before, you can transform data sets from asynchronous calls into results that the ViewModel can manipulate and manage. This is where a strong understanding of the operators will come in handy. Here you can manipulate the stream without breaking it.
In the code example above, you can see a simple case of how the transformer takes the data set results from a service API call, translates them into results, then returns them to the ViewModel to be processed by the scan.
Old Man Listener
Like an old man with a megaphone in his ear, the end of the main pipe you subscribe to the UiModel on the main UI thread. The subscription happens in the ViewController. Here you’ll take the UiModel and process its results for updating the UI. All pretty straight forward.
We sleep safely at night because we write unit tests. With reactive architecture you can now focus more on behavior testing, instead of implementation testing individual operators. You can write tests where you generally don’t care about a specific operator, but instead focus on inputs and outputs at specific moments in your stream.
Here’s an example of testing the main pipe. You’ll test that actions are correctly created and test the scans correctly output UiModels.
Sometimes, you have transformers that are not straight forward. You could also isolate them and unit test them as well by sticking them in their own files.
Even though the android view model is a step in the right direction we still need to remember the memory constraints in Android. Once an activity is removed by the operating system, we need to save our screen state. The best way to do this is to directly save the UiModel inside the
onSaveInstanceState. Store the UiModel in the bundle and reuse in cases where the ViewModel was created for the very first time. Here at this time we could “restore” our state.
Turning up the complexity I added restore logic. What I mean by this is that the UiModel’s most important data, like pageNumber, is saved. The list of now playing movies is not saved because there is only so much room in a bundle. However, only minor changes are needed to handle restore logic.
1.) Save the UiModel to bundle in
2.) Restore the UiModel and pass it to the view model on every use.
3.) Use the saved UiModel as the initial seed in the scan.
All is well with the world…until!
When constructing an application we rarely have all the requirements up front. That’s normal. More typically we need to adapt to new user requirements. A well thought out, documented, and maintained architecture should be able to handle any new requirements thrown at it. Reactive architecture is no different. So let’s go through a more complicated use case.
As a user, I want to be able to filter my results
The more things change, the more they stay the same. Adding filtering requirements should not change the basic flow through my four analogies above. What changes instead are enhancements in the Interactor, and complexity of the logic in both the scan and the old man subscriber.
Thinking in images
Before trying to hash out a new requirement in code, it pays to sit down and lay it out.
The code solution is on the master branch of my github example.
Example Wrap Up
All of the discussions presented were accomplished by slowly cranking the dial of complexity. First, I started out with a basic example that included only the four main components. Second, I addressed application restore. Finally I went full steam ahead with a complicated multi event example. Each of them can be found as branches in my github repo.
Reactive architecture is the next evolutionary architectural step for iOS and Android platforms. The ReactiveX movement set the motion with their cross language APIs. Don’t be fooled thinking this architecture belongs only to Android. Instead, this architecture can be implemented on any mobile, tablet, TV, or web platform. So, I urge you to build those bridges instead of walls, and work together in order to execute the best reactive applications you can. Our customers deserve nothing less.
iOS anyone? Show your iOS team the architecture on my github.
Feel free to follow me on Twitter, and Medium.