The Functional Reactive Architecture Pattern and Its Application to Android Apps
November 4, 2016
LinkedIn’s Talent Solutions team has released two Android apps, the LinkedIn Job Search app and the LinkedIn Students app. Both apps were built leveraging the Functional Reactive Architecture pattern. In this blog post, we will dive into the details of this pattern and discuss how it benefits app developers.
In a nutshell, the Functional Reactive Architecture pattern is a higher-order functions empowered reactive data-driven architecture. It is composed of: the Repository pattern, a Functional Reactive Framework (FRF), and one of several common view patterns, such as the MVP pattern or the MVVM pattern. The FRF bridges the view pattern and the Repository pattern, serving as both controller and dispatcher.
The Repository pattern becomes very important in the Functional Reactive Architecture pattern because it is dramatically boosted by the FRF. Specifically:
- Any data can be converted to and interpreted as a reactive repository. Such data, for example, includes rest call response, screen on/off event, touch event, property change event, etc.
- A virtual repository can be created via composition of any number of existing repositories and arbitrary business logic can be applied onto the composition.
- The view pattern can satisfy the product requirement by a generic approach that first decomposes all data a view needs into fine-grained reactive repositories, then composes those repositories into a virtual repository.
Fig. 1: Functional Reactive Architecture pattern
In the next sections, we will use real-world examples to illustrate how the Functional Reactive Architecture pattern was used to build our apps. We also discuss the concepts of a virtual reactive repository and complex notifications.
Building a JobPosting repository
The Repository pattern is known to be good at isolating the logic that retrieves the data and the business logic that acts on the data. This effectively makes the business logic agnostic to the data source layer. The Repository pattern provides flexibility when the overall design of the application evolves, and provides a substitution point for the unit tests.
As a warmup, let’s start with building a JobPosting repository. Conceptually, a JobPosting repository is capable of supplying job posting information pertaining to any jobId. It is a very basic and very crucial feature of the apps we’ve built.
Implementation-wise, the JobPosting repository is a contract defined as Observable<JobPosting> between the reactive repository and its observer. This contract guarantees that an observer of this repository will be notified with either a JobPosting POJO (plain old Java object) in the happy path, or be notified with a throwable in the error case.
Implementation of the JobPosting repository can be as simple as the following.
In the above code snippet, we use RxJava, a FRF instantiation, to instantiate Observable<JobPosting>, and we use Rest.li, a REST framework implementation, to issue REST requests. What the code does is: pass the jobId to RestliClientProvider, which then creates a Rest.li client for the purpose of fetching the corresponding JobPosting.
With the repository ready, let’s say there is a UI view that is capable of rendering the JobPosting. The view pattern, e.g., MVP, can subscribe an observer (the render) to the jobPostingRepository as the code below does.
As a result, whenever the jobPostingRepository has a JobPosting POJO ready, the render is notified and renders the JobPosting on the UI view. Or, in the unhappy path, an error is notified to the render observer, and the null state is rendered on the UI view.
So far, so good. But cautious readers familiar with app development might already notice that the Rest.li client may be blocking on the main thread, while on the other hand, the render may be painting on any thread (but not the main thread). Both cases would be bad. To work around this, we can use concurrency management. It is a built-in feature of FRFs, such as RxJava. The revised code below creates a jobPostingRepository that makes a blocking REST call on an I/O thread pool, and renders results to the UI view on the main thread.
The only change we have made is using the observeOn function to redirect the functions after it to the schedulers that observeOn specifies. Therefore, in the above code, the map function will grab an I/O thread and use it to issue the blocking REST call. On the other hand, when notifying the observer with a JobPosting POJO, there will be a context switch from the I/O thread to the main thread, and the render will be invoked on the main thread, not on the I/O thread.
Converting a UI event into a repository
So far, we have shown how to build a JobPosting repository given a Rest.li client that is capable of fetching job posting infomation. Now, we will discuss how to convert a UI event into a repository.
The UI event repository we are going to build is bound to a UI textView, and it can notify its observer of the text in the textView whenever there is a change to the text.
Given a textView, the above code registers a textWatcher as a textChangedListener to the textView, such that onTextChanged, the TextWatcher will be invoked. What makes the standard listener into a reactive repository is the emitter. OnTextChanged, the text in the textView will be passed to the emitter, and the text will then flow to the observer of textViewChangedTextRepository. Our textViewChangedTextRepository now can reactively notify its observers when there is a change to the textView’s text. To test this repository, one can simply do this:
In the real world, we can do much more than just print a string with this repository. For example, we use this textViewChangedTextRepository as a building block for the location typeahead of the LinkedIn Students app. When a member types in the textView, the text is sent to a frontend for possible typeahead suggested results, and on receiving the typeahead suggestions, they are rendered in a listView. The code snippet can look like:
So far, given a textView that is editable, and a Rest.li client that is capable of fetching location typeahead suggestions, we have built a generic textViewChangedTextRepository and a more specialized locationTypeAheadOnChangedTextRepository.
The textViewChangedTextRepository and locationTypeAheadOnChangedTextRepository can be further improved with powerful features, such as throttling, dedup, and freshness, as the code below illustrates.
Throttling is the result of the debounce function. For instance, if the member types in “m” and within 200ms types in “o,” and within another 200ms types in “u,” the only text that will flow to the distinctUntilChanged function is “mou.” In other words, just “m” and “mo” will not be emitted.
Dedup is the result of the distinctUntilChanged function. For instance, when the text “mou” has flown to the Rest.li client and a REST call is ongoing, after 200ms if the member types in “n” then immediately deletes “n,” the new text “mou” will pass the throttling and flow to the distinctUntilChanged function, and the new “mou” text will be discarded by it. A location typeahead suggestion REST call will not be initiated for the second “mou.”
Freshness is the result of the switchMap function. For instance, suppose the Rest.li client is querying location typeahead suggestions for text “mou” and the REST call takes longer than usual due to temporary network congestion. After 200ms, when more text (e.g., “mountain view”) flows into the switchMap function, the switchMap will first unsubscribe the render from the results of the ongoing REST call for “mou,” then re-subscribe the render to the new REST call for “mountain view.” From a user experience point of view, the member will only see the location typeahead suggestions for “mountain view.” Although there are in fact two REST calls running in parallel, and both intend to render the location typeahead suggestions they get, the location typeahead suggestions for the text “mou” will never be shown, regardless of which REST call finishes first.
Creating a virtual repository
Among the many features that LinkedIn offers to its members is one that allows members to save/follow many entities, such as jobs, companies, people, roles, articles, locations, school events, etc. For each entity, there is a generic frontend API readily available. For example, there is a saved jobs rest service, saved companies rest service, and so on. These generic frontend APIs are necessary for tracking each saved entity. For example, the LinkedIn Job Search app helps a LinkedIn member to track saved jobs and applied jobs. Based on what we have discussed in previous sections, it should be easy to imagine that two repositories, savedJobPostingRepository and appliedJobPostingRepository, have been implemented leveraging Rest.li clients supplied by the RestClientProvider: newSavedJobPostingClient() and newAppliedJobPostingClient(), respectively.
However, as the figure below illustrates, in the LinkedIn Students app, there is a “My Stuff” page that offers the member a summarized view of all the entities that the member has saved with LinkedIn.
Fig. 2: My Stuff Page
All the saved entities must be fetched from the frontend so as to correctly render the page. How can we do this leveraging existing frontend APIs and the already-implemented repositories, i.e., the savedJobPostingRepository and the appliedJobPostingRepository?
One possible approach is to create a new frontend API, for example, a “MyStuff” REST frontend service, that fans out to the saved entity APIs, aggregates them, and then responds with a MyStuff POJO. Another possible approach is to create a new generic API, namely a Mux REST service, that allows the Rest.li client to specify multiple sub REST calls with some sort of ordering. The generic API implementation would then execute the sub calls according to the ordering, and respond with a generic data model that contained REST call responses for each sub call.
Both of these approaches have some problems, however. The first approach essentially creates a view-based API that is hard to evolve and hard to maintain. The second approach is not view-based and is in this regard much better than the first approach. But the second approach does not readily allow you to express complex logic to be applied to the sub REST calls, such as retry, fallback, etc. Lastly, neither approach is capable of reusing the already-implemented repositories, like the savedJobPostingRepository and appliedJobPostingRepository.
A well-done architecture pattern should provide a more generic, more powerful, and more convenient approach. Luckily, the FRF and reactive repository work together and yield a better solution: the virtual repository approach.
For the sake of code snippet readability, below we show how to create a virtual myStuffRepository that consists only of a savedJobPosting and an appliedJobPosting:
The merge function and the reduce function can linearly scale to accommodate other repositories that are required by the design of the “My Stuff” page, such as savedCompanyRepository, followedPeopleRepository, savedArticleRepository, savedLocationRepository, etc. Therefore, the virtual repository approach is generic and convenient to satisfying the product requirements for the “My Stuff” page.
Next, we use one more example scenario to show some powerful features of the virtual repository approach.
Suppose a product requirement of the “My Stuff” page is that if the app somehow cannot query the savedJobPostingRepository, the entire “My Stuff” page cannot show the null state—instead, only the applied jobs card should render. Moreover, appliedJobPostings is so important for this page that it’s desirable to retry an extra time in case of a failure when querying the appliedJobPostingRepository. The revised myStuffRepository below satisfies the product requirements:
The LinkedIn Student app implemented its homepage view using both the view-based API approach and the virtual repository approach. With the view-based approach, the homepage view’s data loading time is 3.5 seconds. The virtual repository approach reduces the homepage view’s data loading time to 2.1 seconds, a 40% gain.
We built the LinkedIn Job Search app and the LinkedIn Students app using the Functional Reactive Architecture pattern. This architecture pattern has an FRF bridge, a view pattern, and the Repository pattern. With this architecture pattern, UI events can be created as a reactive repository, multiple repositories can be merged and reduced into a virtual repository, and almost arbitrarily complex business logic can be applied to the repository easily. As a result, the view presentation becomes a function of the state and properties, completely passive, making it easier to understand. Also, because the data source and business logic are perfectly isolated from the view presentation layer, the view presentation layer can be as thin as possible, and the data source can be generic and agnostic to evolving design and product requirements.
Special thanks to Manish Baldua for his help with this blog post, and to Sameer Sayed for his constructive review comments when we were exploring the architecture pattern for the LinkedIn Job Search app.