Hey, we have hidden a small bug in the article. If you spot it, we'd love to talk! Reach out to Luke.
This article talks about how we build mobile architecture at Freetrade and shares some of our best practices.
When I first joined Freetrade, I had no mobile experience. This guide became very important, very quickly as I learned the mobile development ropes.
It was written by my onboarding mentor Alex Curran, who was a lifesaver, so I thought we might as well share it with the world!
At Freetrade we build our native apps using Kotlin/RxKotlin for Android and Swift/RxSwift for iOS.
While both apps have a similar architecture, we do it this way to make it easier for developers to transition between them and keep our product consistent across platforms.
Before diving into the article, I would suggest checking out:
As you go through this guide we’d suggest you jump between the different sections so that you can fully understand how the components interact with each other.
Let's start by looking at this diagram to understand the overall architecture of our apps:
If we had to caption the first part of the diagram in a few words, we could say:
The ViewModel "stamps" a ViewState onto the View every time the underlying data changes.
I like to think of View as the ‘closest’ component to the user. These are things like Activities or Fragments in Android, and Views or View Controllers in iOS.
Views should generally be responsible for:
Ideally, Views should be basic and have minimal business logic, as they are hard to test. We tend to move as much of the logic as we can to ViewModels for this reason.
As a general rule, this is the only layer that should be dependent on anything specific to Android or iOS.
These are simple data classes or structs which contain data to display on the UI. They should have no state (i.e. they are never mutated).
Probably the chunkiest bit of our code. ViewModels generally transform data into something the user can see and understand. They do this by creating view states that are passed on to the Views.
As an example:
In the app, you can see a button to switch the account. In the ViewModel (RateOfReturnViewModel below), we find out what accounts the user has and if they have more than one, this ViewModel tells the View Layer to show an account switcher button.
ViewModels should be easily testable, so should be tested! They expose streams for the view layer to consume, so we can use them in unit testing.
These abstract the iOS or Android specific logic of moving from screen to screen away from our architecture, making it easy to test.
Routers can contain business logic like ViewModels but only if it relates to navigation (e.g. moving to different screens depending on some logic).
It’s a good idea to make sure that if routers listen to a database or do an API request, they let the UI know they are doing so. That way the user is aware something is going on behind the scenes. This can then be translated into a loading status, by other layers.
Be careful of using Observables in Routers. Listening to a stream to understand where to route a user to is common but you must be careful to only take one value from the stream. Otherwise, any changes to the data in the backend would trigger a new navigation!
On Android, you can use the registerForActivityResult/launch code to pass data from screen to screen.
For iOS, make sure your ViewController has callbacks in its initialiser, which it will call once it has completed doing whatever it needs to. The router can then route based on that result.
It's a good idea to avoid the ViewController knowing about the routers they're "contained" in because this makes it harder to reuse them.
This is a thin layer that transforms inputs from various external sources (Realtime Databases, APIs, etc) into validated domain objects that can drive business logic.
Any object that is emitted from a Service should be fully valid.
Classes in this layer are often called Services, or sometimes on iOS, Managers.
There are a few reasons why we do it this way.
It helps us keep the code testable and consistent between platforms.
ReactiveX can become very messy, very quickly, but I’ve found that thinking about these layers tends to help me understand where to emit and observe values. With time, these decisions became more intuitive.
While this architecture means that we have more layers to set up and maintain, we’ve also found that it helps us scale the apps and ramp up new engineers quickly, especially for those without strong mobile experience when joining Freetrade - like me!
Did you spot the bug? If you did don’t forget to get in touch with Luke.
If you’d like to learn more about our mobile team at Freetrade or our mission to get everyone investing, get in touch.
We're always on the lookout for engineers to help build our platform and mobile apps.
If you want to help build a world where everyone is investing, come join the fun at freetrade.io/careers.