Senior Software Engineer Jimmy Thompson takes you through the three layers of the Freetrade app
Today I want to talk about a key element of our stack: our mobile apps. Of all the technical components that make up Freetrade, the app is entirely unique. It’s the primary way our customers manage their money and their investments.
If you build mobile applications, something you'll implement often is a way to fetch data, usually from some kind of API, and display it on the screen. You'll also likely need to implement a way to keep the content on that screen up-to-date while the screen is open.
The most basic way to do this is a refresh button, found in most web browsers. Other apps can let you pull-to-refresh, or secretly poll for updates in the background. Whichever you choose has a drastic effect on how the app feels.
While we don't really want to perpetuate the stereotype of flashing red and green tickers, and the kind of investing that's associated with that, it's important that customers feel connected to the stock market when making their investments.
To do this, our apps largely rely on Firebase and reactive programming.
There are already great introductions to reactive programming, but to summarise, it's all about having the mobile app update the UI based on a stream of changes from the backend. We use a framework called ReactiveX, which has equivalent implementations in Swift and Java/Kotlin.
Our apps are structured in three layers:
Services at Freetrade are where most of the data-fetching magic happens. They're generally separated by domain, like the InstrumentPricingService, which is responsible for fetching the latest prices for a given stock.
Thankfully, maintaining the current price of a stock is pretty simple. We rely heavily on Firebase, which plugs into our reactive codebase without much effort. When we call the currentPrice method on the InstrumentPricingService, it subscribes to the prices collection on Firebase, and gives us an Observable stream. When we save a new set of prices in Firebase, it notifies the InstrumentPricingService within the app. This effectively allows us to push the new price to all the screens which are currently viewing it.
View Models at Freetrade exist behind every screen in the app, as well as several individual components. They're responsible for deciding what data needs to be fetched, and what format it should be in, to make that view render properly.
At their simplest, they often just take data from the Services and push it to the View. However, they can also handle things like user input, pagination, sorting and navigating between screens.
The role of this particular View Model is to present a small summary of a stock. You might recognise it from several locations throughout the app. In order to display properly it needs several pieces of data, including the name of the stock and the logo. All we care about right now, however, is the current price.
View Models are where the bulk of the logic sits within the app. This is something we actively try to promote, as it sits between fetching data from Firebase, like the Services, and controlling UI elements, like the View Controllers and the Activities. Both of these are hard to test. By pushing logic into the View Models, and constraining all the inputs and outputs to be Observable streams, we’re able to keep the bulk of our mobile app code under unit tests.
The final part of this puzzle, known as a View Controller on iOS and an Activity on Android, is all about managing the user interface. Now the Service has fetched the data, and the View Model has converted it into something useful, we can display our summary to the user.
We declare our price label, which sits on the right of the summary view. We then subscribe to every update to the View Model’s currentPrice field. Whenever the price changes, the View Controller is notified and adjusts the label to reflect the new price.
The last piece of this puzzle is house-keeping: how we’re disposing of subscriptions we no longer need. We only want to keep the numbers fresh on the screen that the user is currently viewing, anything more wastes the battery and mobile data. To prevent this, we tie our subscriptions to the lifecycle of the screen (or component) that requires them. When those screens are gone, the subscriptions are all cleaned up.
In ReactiveX, you clean up used subscriptions by disposing of them. In the example above, we throw all of our subscriptions into a Dispose Bag. When the view is closed, the View Controller is destroyed, and the Dispose Bag with it. When the Dispose Bag is destroyed, it cleans up all the subscriptions that were placed within it, closing the socket and preventing any further updates.
By having our interface react to changes happening further down our mobile stack, and by relying on Firebase to notify apps when things change, we've achieved a truly reactive mobile app with little effort.
This allows us to build UI components which are reusable, specifying their own data requirements, while ensuring values like the price of a stock are consistent everywhere they are shown. Other approaches usually require you to manually notify other views that the data has refreshed.
So far, we’ve used Firebase for all of our data storage and data requirements within the app. As we've grown, we've made great use of it, but have also brushed against its limitations. Running an entirely reactive setup is a great time saver, but does require maintenance. Tools like Firebase can also cost a lot of money as your user base grows.
Having our mobile apps reading directly from Firebase has meant our database effectively doubles as a public API. This hasn’t really been an issue, but when we have to make breaking changes to a data structure in Firebase, we’re limited by an app release. This means reviews from Apple and Google, and a complete roll out of both of them.
We’ve been making good progress decoupling our apps from our backend schema. Our favourite involves using public and private collections. This means our backend interacts with a private collection, which is then used to regenerate a public collection read by the mobile apps.
For content that changes rarely, such as the user's name and address, we’ve realised we didn't really need to have the mobile app subscribe for updates to this data in real-time. For information like this, we’ve begun fetching it from a HTTP API. Thankfully our network libraries, Alamofire (iOS) and Retrofit (Android), also play very nicely with ReactiveX. Our layered architecture has meant we can swap how we fetch this data from within the Instrument Pricing Service without really changing any other part of the app. We’ll go through more on this in a future post.
We're always on the lookout for engineers to help build our mobile apps. If the above sounds fun, check out our careers page to find out more.