How Imprint built a modern and robust banking mobile app
Background
We launched the Imprint mobile app on iOS and Android in just three months, and it was an incredible and unforgettable journey.
Building a mobile app is not easy, especially since we always look for faster, easier, and more consistent ways to develop and ship mobile experiences across multiple platforms (Android and iOS). Iterating faster means the team can experiment more and provide a better user experience to their end-users. To enable faster iteration, we must carefully design the application’s architecture to maximize development velocity and minimize developer roadblocks.
This article will list a few technical challenges the Imprint mobile engineering team considered when designing and architecting the application. We hope it helps you to understand more about what makes an excellent mobile app.
Mobile challenges
State management
State management is one of the biggest headaches for native mobile development. If not all states are covered for a particular flow or logic within the app, it can lead to unexpected results or even a crash in certain situations. And what is even worse — when an intermittent state issue arises due to a race condition within the app — it may be difficult for engineers to consistently reproduce the issue to then resolve it.
Examples of app-level lifecycle transitions are the app pausing and going to the background, returning to the foreground, or being suspended. They are very common transitions when users use the app, like a push notification or phone call that could trigger the app to go to the background unexpectedly. And if the event is not handled well, it could cause the app to crash if it tries to touch UI.
Most modern mobile apps use event-driven development. In other words, those events trigger app changes. In most cases, it triggers asynchronously, which could provide a better user experience. It includes application state changes, network requests return, push notifications, or user interactions. Most bugs or unexpected crashes are usually caused by an unexpected combination of those events and the application’s state getting conflicts.
State becoming corrupted is a common problem with apps where global or local states are manipulated by multiple components unbeknown to each other.
To resolve the state management issue, we started to introduce reactive programming. Reactive programming is an asynchronous programming paradigm oriented around data streams and the propagation of change. One of the most important advantages is unidirectional data flow (also known as one-way data flow), which means the data has one and only one way to be transferred to other parts of the application. It would help to deal with a large and stateful app to isolate state changes. The app keeps the state as immutable as possible, storing models as immutable objects that emit state change to avoid any state from being corrupted. For instance, we started using Combine for iOS development since it is first-class reactive programming for the Apple platforms without needing external dependencies like RxSwift or ReactiveSwift.
Separation of concerns
In the engineering world, separation of concerns is a design principle for separating a piece of code into distinct sections, an architectural way to avoid stepping on each other’s toes.
For small or straightforward apps, usually, any architecture pattern will do. Though, for larger enterprise applications, it’s crucial to design an architecture pattern that scales for your team’s needs. Since even blindly using Apple’s recommended MVC approach, you can easily find UIViewControllers containing 5,000+ lines of code.
The trouble starts when more than one engineer modifies the same screen simultaneously. With a large team, exemplary architecture is a way to control the level of isolation between engineers and components, limiting overlaps and accidental conflicts while increasing complexity and lines of code.
There are many architectures to achieve the same goal. At Imprint, we adopted the MVVM (Model-View-ViewModel) architecture pattern for the following reasons:
- Most popular. For Android, Google has recommended MVVM architecture with encapsulated components for building robust, production-quality applications. And it is also the most famous architecture in the Android world. On the other hand, for iOS, we want to keep the architecture consistent between the two platforms with many benefits, which we can discuss in another article.
- Ease of testing. MVVM breaks the coupling between the application logic and the UI and so makes testing more accessible. It should be optional to do all testing via the UI so tests become quicker and easier to set up and run.
- Code separation. With MVVM, each screen or UI component could be an MVVM module, and the code is well separated, so other code changes would be restricted within the module and have less side impact.
- Strong code and feature ownership. Engineers can leverage the code owner file to have strong ownership for each module. It would be helpful once you have more than one engineer on your mobile team.
Navigation
Navigation is one of the biggest challenges for any decent-sized mobile application. A well-defined app navigation strategy is essential to building a flexible, scalable application. It can also make each UI component reusable and make any flow flexible, providing the ability to do A/B testing for any predefined flow.
In traditional MVVM or other architectures, the definition for navigation is always living inside each module. Therefore, it is hard to reuse the screen since the navigation logic is coupled inside the view model.
View controllers work best when they stand alone in your app, unaware of their position in the app’s flow or even that they are part of a flow in the first place. Not only does this help make the code easier to test and reason about, but it also allows us to reuse view controllers elsewhere in the app more easily.
At Imprint, we are using a coordinator pattern to define the navigation flow. The coordinator pattern provides an encapsulation of navigation logic to decouple view controllers from one another. In other words: instead of pushing and presenting ViewControllers from other view controllers, all the screens’ navigation will be managed by coordinators. With the coordinator pattern, all navigation-related code should be live in the coordinator instead of each view model. The only component that knows about view controllers directly is the coordinator. Consequently, view controllers are much more reusable. In addition, it provides a flexible way for applications to navigate different screens based on business logic or A/B testing needs.
During credit card applications, for example, to improve the funnel conversion rate, different segment users may see extra steps in a separate order in the application flow — all controlled by one coordinator class.
What is next
We keep exploring and innovating ways to improve our productivity and quality and shorten the app release cycle for the Imprint mobile app.
While Imprint’s technologies and challenges will likely change, our mission and culture of overcoming them will last. Want to be a part of it? Please find us at talent@imprint.co or imprint.co/careers.